Skip to content

Commit

Permalink
Merge pull request #2679 from square/py/fix_android_growth_crash
Browse files Browse the repository at this point in the history
Fix warmup for Android heap growth
  • Loading branch information
pyricau authored May 21, 2024
2 parents d65afba + 78a735c commit 075663e
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 24 deletions.
10 changes: 9 additions & 1 deletion leakcanary/leakcanary-android-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ dependencies {
api projects.leakcanary.leakcanaryCore
api projects.leakcanary.leakcanaryAndroidUtils
api projects.shark.sharkAndroid

androidTestImplementation libs.androidX.multidex
androidTestImplementation libs.androidX.test.core
androidTestImplementation libs.androidX.test.runner
androidTestImplementation libs.assertjCore
}

android {
compileSdk versions.compileSdk
defaultConfig {
targetSdk versions.compileSdk
minSdk versions.minSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
buildFeatures.buildConfig = false
namespace 'com.squareup.leakcanary.core'
namespace 'com.squareup.leakcanary.android.test'
testNamespace 'com.squareup.leakcanary.android.test.test'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name="androidx.multidex.MultiDexApplication"
>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package leakcanary

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import shark.ObjectGrowthDetector
import shark.RepeatingScenarioObjectGrowthDetector.Companion.DEFAULT_MAX_HEAP_DUMPS
import shark.RepeatingScenarioObjectGrowthDetector.Companion.IN_PROCESS_SCENARIO_LOOPS_PER_DUMP
import shark.forAndroidHeap

class RepeatingAndroidInProcessScenarioTest {

private val growingList = mutableListOf<String>()

@Test fun failing_scenario_iterates_to_max_heap_dumps_times_loops_per_dump() {
val detector = ObjectGrowthDetector.forAndroidHeap()
.repeatingAndroidInProcessScenario()
var iteration = 0
detector.findRepeatedlyGrowingObjects {
growingList += "growth $iteration"
iteration++
}

assertThat(iteration).isEqualTo(DEFAULT_MAX_HEAP_DUMPS * IN_PROCESS_SCENARIO_LOOPS_PER_DUMP)
}

@Test fun failing_scenario_finds_expected_growing_object() {
val detector = ObjectGrowthDetector.forAndroidHeap()
.repeatingAndroidInProcessScenario()
var iteration = 0
val heapGrowth = detector.findRepeatedlyGrowingObjects {
growingList += "growth $iteration"
iteration++
}

val growingObject = heapGrowth.growingObjects.single()
assertThat(growingObject.name).startsWith("INSTANCE_FIELD RepeatingAndroidInProcessScenarioTest.growingList")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import shark.repeatingScenario
*/
fun ObjectGrowthDetector.repeatingAndroidInProcessScenario(
maxHeapDumps: Int = RepeatingScenarioObjectGrowthDetector.DEFAULT_MAX_HEAP_DUMPS,
// In process => More than one to account for the impact of running the analysis.
scenarioLoopsPerDump: Int = 2,
scenarioLoopsPerDump: Int = RepeatingScenarioObjectGrowthDetector.IN_PROCESS_SCENARIO_LOOPS_PER_DUMP,
): RepeatingScenarioObjectGrowthDetector {
return repeatingScenario(
heapGraphProvider = HeapGraphProvider.dumpingAndDeleting(
heapDumper = HeapDumper.forAndroidInProcess()
.withGc(gcTrigger = GcTrigger.inProcess())
.withDetectorWarmup(this),
.withDetectorWarmup(this, androidHeap = true),
heapDumpFileProvider = HeapDumpFileProvider.tempFile()
),
maxHeapDumps = maxHeapDumps,
Expand Down
4 changes: 2 additions & 2 deletions leakcanary/leakcanary-core/api/leakcanary-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ public final class leakcanary/HeapDumperKt {

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 <init> (Lshark/ObjectGrowthDetector;Lleakcanary/HeapDumper;Z)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 static final fun withDetectorWarmup (Lleakcanary/HeapDumper;Lshark/ObjectGrowthDetector;Z)Lleakcanary/HeapDumper;
}

public final class leakcanary/TempHeapDumpFileProvider : leakcanary/HeapDumpFileProvider {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,68 @@ import okio.ByteString.Companion.toByteString
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import shark.HprofHeader
import shark.HprofWriterHelper
import shark.ValueHolder
import shark.dumpToBytes

class ObjectGrowthWarmupHeapDumperTest {

@Test fun `heap dump 1 as hex constant matches generated heap dump hex`() {
assertThat(ObjectGrowthWarmupHeapDumper.heapDump1Hex()).isEqualTo(dumpGrowingListHeapAsHex(1))
assertThat(ObjectGrowthWarmupHeapDumper.heapDump1Hex(androidHeap = false)).isEqualTo(dumpGrowingListHeapAsHex(1))
}

@Test fun `heap dump 2 as hex constant matches generated heap dump hex`() {
assertThat(ObjectGrowthWarmupHeapDumper.heapDump2Hex()).isEqualTo(dumpGrowingListHeapAsHex(2))
assertThat(ObjectGrowthWarmupHeapDumper.heapDump2Hex(androidHeap = false)).isEqualTo(dumpGrowingListHeapAsHex(2))
}

@Test fun `heap dump 3 as hex constant matches generated heap dump hex`() {
assertThat(ObjectGrowthWarmupHeapDumper.heapDump3Hex()).isEqualTo(dumpGrowingListHeapAsHex(3))
assertThat(ObjectGrowthWarmupHeapDumper.heapDump3Hex(androidHeap = false)).isEqualTo(dumpGrowingListHeapAsHex(3))
}

@Test fun `android heap dump 1 as hex constant matches generated heap dump hex`() {
assertThat(ObjectGrowthWarmupHeapDumper.heapDump1Hex(androidHeap = true)).isEqualTo(dumpAndroidGrowingListHeapAsHex(1))
}

@Test fun `android heap dump 2 as hex constant matches generated heap dump hex`() {
assertThat(ObjectGrowthWarmupHeapDumper.heapDump2Hex(androidHeap = true)).isEqualTo(dumpAndroidGrowingListHeapAsHex(2))
}

@Test fun `android heap dump 3 as hex constant matches generated heap dump hex`() {
assertThat(ObjectGrowthWarmupHeapDumper.heapDump3Hex(androidHeap = true)).isEqualTo(dumpAndroidGrowingListHeapAsHex(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)
growingList(listItemCount)
}.toByteString().hex()
}

private fun dumpAndroidGrowingListHeapAsHex(listItemCount: Int): String {
val heapDumpTimestamp = ("0b501e7e" + "ca55e77e").decodeHex().toByteArray().toLong()
return dumpToBytes(hprofHeader = HprofHeader(heapDumpTimestamp = heapDumpTimestamp)) {
"android.os.Build" clazz {
staticField["MANUFACTURER"] = string("M")
staticField["ID"] = string("I")
}
"android.os.Build\$VERSION" clazz {
staticField["SDK_INT"] = ValueHolder.IntHolder(42)
}
growingList(listItemCount)
}.toByteString().hex()
}


private fun HprofWriterHelper.growingList(listItemCount: Int) {
"Holder" clazz {
val refs = (1..listItemCount).map {
instance(objectClassId)
}.toTypedArray()
staticField["list"] = objectArray(*refs)
}
}


private fun ByteArray.toLong(): Long {
check(size == 8)
var pos = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import shark.repeatingScenario
*/
fun ObjectGrowthDetector.repeatingJvmInProcessScenario(
maxHeapDumps: Int = RepeatingScenarioObjectGrowthDetector.DEFAULT_MAX_HEAP_DUMPS,
// In process => More than one to account for the impact of running the analysis.
scenarioLoopsPerDump: Int = 2,
scenarioLoopsPerDump: Int = RepeatingScenarioObjectGrowthDetector.IN_PROCESS_SCENARIO_LOOPS_PER_DUMP,
): RepeatingScenarioObjectGrowthDetector {
return repeatingScenario(
heapGraphProvider = HeapGraphProvider.dumpingAndDeleting(
heapDumper = HeapDumper.forJvmInProcess()
.withGc(gcTrigger = GcTrigger.inProcess())
.withDetectorWarmup(this),
.withDetectorWarmup(this, androidHeap = false),
heapDumpFileProvider = HeapDumpFileProvider.tempFile()
),
maxHeapDumps = maxHeapDumps,
Expand Down
1 change: 1 addition & 0 deletions shark/shark/api/shark.api
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@ public final class shark/RepeatingScenarioObjectGrowthDetector {
public static final field Companion Lshark/RepeatingScenarioObjectGrowthDetector$Companion;
public static final field DEFAULT_MAX_HEAP_DUMPS I
public static final field DEFAULT_SCENARIO_LOOPS_PER_DUMP I
public static final field IN_PROCESS_SCENARIO_LOOPS_PER_DUMP I
public fun <init> (Lshark/HeapGraphProvider;Lshark/ObjectGrowthDetector;II)V
public synthetic fun <init> (Lshark/HeapGraphProvider;Lshark/ObjectGrowthDetector;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun findRepeatedlyGrowingObjects (Lkotlin/jvm/functions/Function0;)Lshark/HeapGrowthTraversal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class RepeatingScenarioObjectGrowthDetector(
companion object {
const val DEFAULT_MAX_HEAP_DUMPS = 5
const val DEFAULT_SCENARIO_LOOPS_PER_DUMP = 1

/**
* In process => More than one to account for the impact of running the analysis.
*/
const val IN_PROCESS_SCENARIO_LOOPS_PER_DUMP = 2
}
}

Expand Down

0 comments on commit 075663e

Please sign in to comment.