blob: 3c113d4db467bdb44601810e0f6f92beeff3ad32 [file] [log] [blame]
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.HeapDiff
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)
val leakies = mutableListOf<Any>()
val stringLeaks = mutableListOf<String>()
var customLeakyLinkedList = CustomLinkedList()
val leakyHashMap = HashMap<String, Any>()
val multiLeakies = mutableListOf<MultiLeaky>()
@get:Rule
val tempFolder = TemporaryFolder()
@Test
fun `empty scenario leads to no heap growth`() {
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
val emptyScenario = {}
val heapTraversal = detector.findRepeatedlyGrowingObjects(
scenarioLoopsPerDump = 1,
roundTripScenario = emptyScenario
)
assertThat(heapTraversal.growingObjects).isEmpty()
}
@Test
fun `leaky increase leads to heap growth`() {
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
leakies += Any()
}
assertThat(heapTraversal.growingObjects).hasSize(1)
}
@Test
fun `string leak increase leads to heap growth`() {
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
var index = 0
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
var index = 0
val heapTraversal = detector.findRepeatedlyGrowingObjects(
maxHeapDumps = maxHeapDumps,
scenarioLoopsPerDump = 1
) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
val heapTraversal =
detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = scenarioLoopsPerDump) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
}
assertThat(heapTraversal.growingObjects).hasSize(1)
}
@Test
fun `custom leaky linked list reports descendant to root as flattened collection`() {
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
)
var index = 0
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeap(),
)
var index = 0
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
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 = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeap(),
)
val heapTraversal = detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
leakies += Any()
}
val growingObject = heapTraversal.growingObjects.single()
assertThat(growingObject.name).contains("leakies")
}
@Test
fun `DeleteOnHeapDumpClose invokes file deletion in between each scenario`() {
// Nothing to delete on first run.
var didDeleteFile = true
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.DeleteOnHeapDumpClose {
it.delete()
didDeleteFile = true
}
)
detector.findRepeatedlyGrowingObjects(scenarioLoopsPerDump = 1) {
assertThat(didDeleteFile).isTrue()
didDeleteFile = false
leakies += Any()
}
assertThat(didDeleteFile).isTrue()
}
@Test
fun `KeepHeapDumps does not delete heap dumps`() {
val heapDumpDirectory = tempFolder.newFolder()
val maxHeapDump = 5
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.KeepHeapDumps,
heapDumpDirectoryProvider = { heapDumpDirectory }
)
detector.findRepeatedlyGrowingObjects(
maxHeapDumps = maxHeapDump,
scenarioLoopsPerDump = 1
) {
leakies += Any()
}
val heapDumpFileCount = heapDumpDirectory.listFiles()!!.count { it.extension == "hprof" }
assertThat(heapDumpFileCount).isEqualTo(maxHeapDump)
}
@Test
fun `KeepHeapDumpsOnObjectsGrowing does invokes file deletion in between each scenario`() {
var didDeleteFile = false
val heapDumpDirectory = tempFolder.newFolder()
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.KeepHeapDumpsOnObjectsGrowing {
didDeleteFile = true
it.delete()
},
heapDumpDirectoryProvider = { heapDumpDirectory }
)
detector.findRepeatedlyGrowingObjects(
maxHeapDumps = 5,
scenarioLoopsPerDump = 1
) {
assertThat(didDeleteFile).isFalse()
leakies += Any()
}
}
@Test
fun `KeepHeapDumpsOnObjectsGrowing does not delete any heap dump if objects growing`() {
val maxHeapDump = 5
val heapDumpDirectory = tempFolder.newFolder()
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.KeepHeapDumpsOnObjectsGrowing(),
heapDumpDirectoryProvider = { heapDumpDirectory }
)
detector.findRepeatedlyGrowingObjects(
maxHeapDumps = maxHeapDump,
scenarioLoopsPerDump = 1
) {
leakies += Any()
}
val heapDumpFileCount = heapDumpDirectory.listFiles()!!.count { it.extension == "hprof" }
assertThat(heapDumpFileCount).isEqualTo(maxHeapDump)
}
@Test
fun `KeepHeapDumpsOnObjectsGrowing invokes file deletion on completion if objects not growing`() {
var filesDeleted = 0
val heapDumpDirectory = tempFolder.newFolder()
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.KeepHeapDumpsOnObjectsGrowing {
filesDeleted++
it.delete()
},
heapDumpDirectoryProvider = { heapDumpDirectory }
)
val leakyScenarioRuns = 3
var i = 1
detector.findRepeatedlyGrowingObjects(
maxHeapDumps = 5,
scenarioLoopsPerDump = 1
) {
assertThat(filesDeleted).isEqualTo(0)
if (i <= leakyScenarioRuns) {
leakies += Any()
}
i++
}
assertThat(filesDeleted).isEqualTo(1 + leakyScenarioRuns)
}
@Test
fun `KeepZippedHeapDumpsOnObjectsGrowing leaves zipped heap dumps if objects growing`() {
val maxHeapDump = 5
val heapDumpDirectory = tempFolder.newFolder()
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.KeepZippedHeapDumpsOnObjectsGrowing(),
heapDumpDirectoryProvider = { heapDumpDirectory }
)
detector.findRepeatedlyGrowingObjects(
maxHeapDumps = maxHeapDump,
scenarioLoopsPerDump = 1
) {
leakies += Any()
}
val heapDumpDirectoryFileExtensions = heapDumpDirectory.listFiles()!!.map { it.extension }
assertThat(heapDumpDirectoryFileExtensions).hasSize(maxHeapDump)
assertThat(heapDumpDirectoryFileExtensions).containsOnly("zip")
}
@Test
fun `KeepZippedHeapDumpsOnObjectsGrowing deletes all files if objects not growing`() {
val heapDumpDirectory = tempFolder.newFolder()
val detector = HeapDiff.repeatingJvmInProcessScenario(
objectGrowthDetector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs(),
heapDumpStorageStrategy = HeapDumpStorageStrategy.KeepZippedHeapDumpsOnObjectsGrowing(),
heapDumpDirectoryProvider = { heapDumpDirectory }
)
val leakyScenarioRuns = 3
var i = 1
detector.findRepeatedlyGrowingObjects(
maxHeapDumps = 5,
scenarioLoopsPerDump = 1
) {
if (i <= leakyScenarioRuns) {
leakies += Any()
}
i++
}
assertThat(heapDumpDirectory.listFiles()).isEmpty()
}
private fun ObjectGrowthDetector.Companion.forJvmHeapNoSyntheticRefs(): ObjectGrowthDetector {
val referenceMatchers = JvmObjectGrowthReferenceMatchers.defaults
return ObjectGrowthDetector(
gcRootProvider = MatchingGcRootProvider(JvmObjectGrowthReferenceMatchers.defaults),
referenceReaderFactory = ActualMatchingReferenceReaderFactory(referenceMatchers)
)
}
}