RetainedSizeTest.kt
TLDR
The RetainedSizeTest.kt
file is a test file that contains test cases for calculating the retained size of objects in a heap dump. It tests various scenarios of leaking instances with different types and structures.
Methods
setUp
This method is annotated with @Before
and is used to set up the test environment before each test case. It creates a temporary file temp.hprof
for storing the heap dump.
emptyLeakingInstance
This method is a test case that verifies the retained size of an empty leaking instance. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance. The retained size is expected to be 0.
leakingInstanceWithPrimitiveType
This method is a test case that verifies the retained size of a leaking instance with a primitive type field. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with a field answer
of type LongHolder
. The retained size is expected to be 8 bytes.
leakingInstanceWithPrimitiveArray
This method is a test case that verifies the retained size of a leaking instance with a primitive array field. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with a field answer
of type CharArray
with a value of "42". The retained size is expected to be 8 bytes.
leakingInstanceWithString
This method is a test case that verifies the retained size of a leaking instance with a string field. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with a field answer
of type String
with a value of "42". The retained size is expected to be 16 bytes.
leakingInstanceWithInstance
This method is a test case that verifies the retained size of a leaking instance with another instance as a field. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with a field answer
of type FortyTwo
which itself contains a field number
of type IntHolder
. The retained size is expected to be 8 bytes.
leakingInstanceWithPrimitiveWrapper
This method is a test case that verifies the retained size of a leaking instance with a primitive wrapper field. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with a field answer
of type java.lang.Integer
which contains a field value
of type IntHolder
. The retained size is expected to be 8 bytes.
leakingInstanceWithPrimitiveWrapperArray
This method is a test case that verifies the retained size of a leaking instance with an array of primitive wrappers. It creates a heap dump containing instances of Integer
with different values and constructs an array with these instances as elements. The array is assigned to the field answer
of a leaking instance. The retained size is expected to be 20 bytes.
leakingInstanceWithObjectArray
This method is a test case that verifies the retained size of a leaking instance with an array of objects. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with a field answer
of type Object[]
with two elements: "Forty" and "Two". The retained size is expected to be 12 bytes.
leakingInstanceWithDeepRetainedObjects
This method is a test case that verifies the retained size of a leaking instance with deeply nested retained objects. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance with nested instances. The retained size is expected to be 24 bytes.
leakingInstanceNotDominating
This method is a test case that verifies the retained size of a leaking instance that is not dominating. It creates a heap dump containing a class GcRoot
with two static fields: shortestPath
pointing to a leaking instance and rootDominator
pointing to the same instance. The retained size is expected to be 4 bytes.
leakingInstanceWithSuperClass
This method is a test case that verifies the retained size of a leaking instance with a superclass. It creates a heap dump containing a class Parent
with a field value
of type LongHolder
and a class Child
with a superclass Parent
and a field value
of type IntHolder
. The heap dump contains a class GcRoot
with a static field shortestPath
pointing to a leaking instance of Child
. The retained size is expected to be 16 bytes.
leakingInstanceDominatedByOther
This method is a test case that verifies the retained size of a leaking instance dominated by other leaking instances. It creates a heap dump containing a class GcRoot
with a static field shortestPath
pointing to a leaking instance GrandParentLeaking
. The leaked instance GrandParentLeaking
has a field child
pointing to a leaking instance ParentLeaking
, which has a field child
pointing to a leaking instance ChildLeaking
. The retained size is expected to be 22 bytes.
crossDominatedIsNotDominated
This method is a test case that verifies the retained size of leaking instances that are cross-dominated. It creates a heap dump containing two classes GcRoot1
and GcRoot2
, each with a static field shortestPath
pointing to the same leaking instance. The retained size of both instances is expected to be 4 bytes.
nativeSizeAccountedFor
This method is a test case that verifies the retained size of a leaking instance with a native size. It creates a heap dump containing a leaking instance of android.graphics.Bitmap
with a width and height, and a reference to the same instance in a libcore.util.NativeAllocationRegistry
. The retained size is expected to be the native size plus the size of the object fields.
thread retained size includes java local references
This method is a test case that verifies the retained size of a leaking instance includes Java local references. It creates a heap dump containing a leaking instance of Thread
with a native reference to a long
array. The retained size is expected to be the size of the long
array.
retainedInstances
This method processes the heap dump file and returns a list of Leak
objects. It uses the hprofFile
to check for leaks and compute the retained heap size.
List<Leak>.firstRetainedSize
This extension function calculates and returns the retained size of the first Leak
object in the list of Leak
objects.
Classes
No classes defined in the file.
package shark
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import java.io.File
import shark.GcRoot.JavaFrame
import shark.GcRoot.ThreadObject
import shark.HeapObject.HeapInstance
class RetainedSizeTest {
@get:Rule
var testFolder = TemporaryFolder()
private lateinit var hprofFile: File
@Before
fun setUp() {
hprofFile = testFolder.newFile("temp.hprof")
}
@Test fun emptyLeakingInstance() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
assertThat(retainedSize).isEqualTo(0)
}
@Test fun leakingInstanceWithPrimitiveType() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = LongHolder(42)
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 8 bytes for long
assertThat(retainedSize).isEqualTo(8)
}
@Test fun leakingInstanceWithPrimitiveArray() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = "42".charArrayDump
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference, 2 bytes per char
assertThat(retainedSize).isEqualTo(8)
}
@Test fun leakingInstanceWithString() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = string("42")
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference, string (4 array ref + 4 int + 2 byte per char)
assertThat(retainedSize).isEqualTo(16)
}
@Test fun leakingInstanceWithInstance() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = "FortyTwo" instance {
field["number"] = IntHolder(42)
}
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference + 4 byte int
assertThat(retainedSize).isEqualTo(8)
}
@Test fun leakingInstanceWithPrimitiveWrapper() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = "java.lang.Integer" instance {
field["value"] = IntHolder(42)
}
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference, int field
assertThat(retainedSize).isEqualTo(8)
}
@Test fun leakingInstanceWithPrimitiveWrapperArray() {
hprofFile.dump {
val intWrapperClass = clazz("java.lang.Integer", fields = listOf("value" to IntHolder::class))
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = objectArrayOf(
intWrapperClass,
instance(
intWrapperClass,
fields = listOf<ValueHolder>(IntHolder(4))
),
instance(
intWrapperClass,
fields = listOf<ValueHolder>(IntHolder(2))
)
)
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference * 3, 2 ints
assertThat(retainedSize).isEqualTo(20)
}
@Test fun leakingInstanceWithObjectArray() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = objectArray("Forty" instance {}, "Two" instance {})
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference, 4 bytes per object entry
assertThat(retainedSize).isEqualTo(12)
}
@Test fun leakingInstanceWithDeepRetainedObjects() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = "Forty" instance {
field["forty"] = "Two" instance {
field["two"] = string("42")
}
}
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference * 3, string (4 array ref + 4 int + 2 byte per char)
assertThat(retainedSize).isEqualTo(24)
}
@Test fun leakingInstanceNotDominating() {
hprofFile.dump {
val fortyTwo = string("42")
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = fortyTwo
}
staticField["rootDominator"] = fortyTwo
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference
assertThat(retainedSize).isEqualTo(4)
}
@Test fun leakingInstanceWithSuperClass() {
hprofFile.dump {
val parentClass = clazz("Parent", fields = listOf("value" to LongHolder::class))
val childClass =
clazz("Child", superclassId = parentClass, fields = listOf("value" to IntHolder::class))
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["answer"] = instance(childClass, listOf(LongHolder(42), IntHolder(42)))
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference + Long + Int
assertThat(retainedSize).isEqualTo(16)
}
@Test fun leakingInstanceDominatedByOther() {
hprofFile.dump {
"GcRoot" clazz {
staticField["shortestPath"] = "GrandParentLeaking" watchedInstance {
field["answer"] = ShortHolder(42)
field["child"] = "ParentLeaking" watchedInstance {
field["answer"] = IntHolder(42)
field["child"] = "ChildLeaking" watchedInstance {
field["answer"] = LongHolder(42)
}
}
}
}
}
val retainedInstances = retainedInstances()
require(retainedInstances.size == 1)
val instance = retainedInstances[0]
assertThat(instance.leakTraces.first().leakingObject.className).isEqualTo("GrandParentLeaking")
// 4 bytes per ref * 2 + short + int + long
assertThat(instance.totalRetainedHeapByteSize).isEqualTo(22)
}
@Test fun crossDominatedIsNotDominated() {
hprofFile.dump {
val fortyTwo = string("42")
"GcRoot1" clazz {
staticField["shortestPath"] = "Leaking1" watchedInstance {
field["answer"] = fortyTwo
}
}
"GcRoot2" clazz {
staticField["shortestPath"] = "Leaking2" watchedInstance {
field["answer"] = fortyTwo
}
}
}
val retainedInstances = retainedInstances()
require(retainedInstances.size == 2)
retainedInstances.forEach { instance ->
// 4 byte reference
assertThat(instance.totalRetainedHeapByteSize).isEqualTo(4)
}
}
@Test fun nativeSizeAccountedFor() {
val width = 24
val height = 16
// pixel count * 4 bytes per pixel (ARGB_8888)
val nativeBitmapSize = width * height * 4
hprofFile.dump {
val bitmap = "android.graphics.Bitmap" instance {
field["mWidth"] = IntHolder(width)
field["mHeight"] = IntHolder(height)
}
val referenceClass =
clazz("java.lang.ref.Reference", fields = listOf("referent" to ReferenceHolder::class))
val cleanerClass = clazz(
"sun.misc.Cleaner", clazz("java.lang.ref.PhantomReference", referenceClass),
fields = listOf("thunk" to ReferenceHolder::class)
)
instance(
cleanerClass,
fields = listOf("libcore.util.NativeAllocationRegistry\$CleanerThunk" instance {
field["this\$0"] = "libcore.util.NativeAllocationRegistry" instance {
field["size"] = LongHolder(nativeBitmapSize.toLong())
}
}, bitmap)
)
"GcRoot" clazz {
staticField["shortestPath"] = "Leaking" watchedInstance {
field["bitmap"] = bitmap
}
}
}
val retainedSize = retainedInstances()
.firstRetainedSize()
// 4 byte reference + 2 * Int + native size
assertThat(retainedSize).isEqualTo(12 + nativeBitmapSize)
}
@Test fun `thread retained size includes java local references`() {
hprofFile.dump {
val threadInstance = Thread::class.java.name instance { }
gcRoot(
ThreadObject(
id = threadInstance.value,
threadSerialNumber = 42,
stackTraceSerialNumber = 0
)
)
val longArrayId = primitiveLongArray(LongArray(3))
gcRoot(JavaFrame(id = longArrayId, threadSerialNumber = 42, frameNumber = 0))
}
val analysis = hprofFile.checkForLeaks<HeapAnalysis>(
computeRetainedHeapSize = true,
leakingObjectFinder = FilteringLeakingObjectFinder(listOf(FilteringLeakingObjectFinder.LeakingObjectFilter { heapObject ->
heapObject is HeapInstance &&
heapObject.instanceClassName == Thread::class.java.name
}))
)
println(analysis.toString())
analysis as HeapAnalysisSuccess
val retainedInstances = analysis.applicationLeaks
val retainedSize = retainedInstances.firstRetainedSize()
// LongArray(3), 8 bytes per long
assertThat(retainedSize).isEqualTo(3 * 8)
}
private fun retainedInstances(): List<Leak> {
val analysis = hprofFile.checkForLeaks<HeapAnalysis>(computeRetainedHeapSize = true)
println(analysis.toString())
analysis as HeapAnalysisSuccess
return analysis.applicationLeaks
}
private fun List<Leak>.firstRetainedSize(): Int {
return map { it.totalRetainedHeapByteSize!! }
.first()
}
}