main

square/leakcanary

Last updated at: 29/12/2023 09:47

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()
  }
}