main

square/leakcanary

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

HprofWriterHelper.kt

TLDR

The HprofWriterHelper.kt file is a Kotlin file that defines a class called HprofWriterHelper which is used for writing Hprof files. It provides methods for creating class definitions, instances, and various types of arrays in the Hprof file. The file also includes utility methods for dumping the generated Hprof data to a file or to bytes.

Classes

HprofWriterHelper

The HprofWriterHelper class is used for writing Hprof files. It provides methods for creating class definitions, instances, arrays, and references in the Hprof file. It also implements the Closeable interface to allow closing the writer.

Inner classes

  • InstanceAndClassDefinition: Represents a class definition and instance definition. It provides methods for defining fields and static fields for class definitions, and instance fields for instance definitions.
  • ClassDefinition: Represents a class definition. It provides a method for defining static fields.

Methods

clazz

This method is used for creating a class definition in the Hprof file. It takes parameters like className, superclassId, staticFields, and fields to define the class. It returns the ID of the created class.

stringRecord

This method is used for creating a string record in the Hprof file. It takes a name parameter and returns the created string record.

gcRoot

This method is used for creating a garbage root reference in the Hprof file. It takes a gcRoot parameter and writes the corresponding record to the file.

arrayClass

This method is used for creating an array class definition in the Hprof file. It takes a className parameter and returns the ID of the created array class.

string

This method is used for creating a string instance in the Hprof file. It takes a string parameter and returns the corresponding reference.

keyedWeakReference

This method is used for creating a keyed weak reference instance in the Hprof file. It takes a referentInstanceId parameter and returns the corresponding reference.

instance

This method is used for creating a general instance in the Hprof file. It takes a classId parameter and fields parameter and returns the corresponding reference.

instanceAndClassDefinition

This inner class represents a combined instance and class definition. It provides methods for defining fields for both class and instance definitions.

dump

The dump function is used for dumping the generated Hprof data to a file. It takes an optional hprofHeader parameter and a block parameter of the type HprofWriterHelper.() -> Unit that defines the operations to perform on the Hprof file.

dumpToBytes

The dumpToBytes function is used for dumping the generated Hprof data to bytes. It takes an optional hprofHeader parameter and a block parameter of the type HprofWriterHelper.() -> Unit that defines the operations to perform on the Hprof file.

asHprofBytes

This extension function is used for converting a list of Hprof records to bytes. It returns a DualSourceProvider object that encapsulates the byte array.

package shark

import okio.Buffer
import shark.GcRoot.StickyClass
import shark.HprofRecord.HeapDumpRecord.GcRootRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import shark.HprofRecord.LoadClassRecord
import shark.HprofRecord.StringRecord
import shark.PrimitiveType.BOOLEAN
import shark.PrimitiveType.BYTE
import shark.PrimitiveType.CHAR
import shark.PrimitiveType.DOUBLE
import shark.PrimitiveType.FLOAT
import shark.PrimitiveType.INT
import shark.PrimitiveType.LONG
import shark.PrimitiveType.SHORT
import shark.ValueHolder.BooleanHolder
import shark.ValueHolder.ByteHolder
import shark.ValueHolder.CharHolder
import shark.ValueHolder.DoubleHolder
import shark.ValueHolder.FloatHolder
import shark.ValueHolder.IntHolder
import shark.ValueHolder.LongHolder
import shark.ValueHolder.ReferenceHolder
import shark.ValueHolder.ShortHolder
import java.io.Closeable
import java.io.File
import java.util.UUID
import kotlin.random.Random
import kotlin.reflect.KClass
import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump

class HprofWriterHelper constructor(
  private val writer: HprofWriter
) : Closeable {

  private var lastId = 0L
  private val id: Long
    get() = ++lastId

  private val weakRefKeyRandom = Random(42)

  // Sequence identical for every test run
  private val weakRefKey: String
    get() =
      UUID(weakRefKeyRandom.nextLong(), weakRefKeyRandom.nextLong()).toString()

  private val typeSizes =
    PrimitiveType.byteSizeByHprofType + (PrimitiveType.REFERENCE_HPROF_TYPE to writer.hprofHeader.identifierByteSize)

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

  private val weakReferenceClassId = clazz(
    className = "java.lang.ref.WeakReference",
    superclassId = referenceClassId
  )
  private val keyedWeakReferenceClassId = clazz(
    superclassId = weakReferenceClassId,
    className = "leakcanary.KeyedWeakReference",
    staticFields = listOf("heapDumpUptimeMillis" to LongHolder(30000)),
    fields = listOf(
      "key" to ReferenceHolder::class,
      "name" to ReferenceHolder::class,
      "watchUptimeMillis" to LongHolder::class,
      "retainedUptimeMillis" to LongHolder::class
    )
  )

  fun clazz(
    className: String,
    superclassId: Long = -1L, // -1 defaults to java.lang.Object
    staticFields: List<Pair<String, ValueHolder>> = emptyList(),
    fields: List<Pair<String, KClass<out ValueHolder>>> = emptyList()
  ): Long {
    val classNameRecord = StringRecord(id, className)
    writer.write(classNameRecord)
    val loadClass = LoadClassRecord(1, id, 1, classNameRecord.id)
    writer.write(loadClass)

    val staticFieldRecords = staticFields.map {
      val fieldName = StringRecord(id, it.first)
      writer.write(fieldName)
      StaticFieldRecord(fieldName.id, typeOf(it.second), it.second)
    }

    val fieldRecords = fields.map {
      val fieldName = StringRecord(id, it.first)
      writer.write(fieldName)
      FieldRecord(fieldName.id, typeOf(it.second))
    }

    var instanceSize = fieldRecords.sumBy {
      typeSizes.getValue(it.type)
    }

    var nextUpId = if (superclassId == -1L) objectClassId else superclassId
    while (nextUpId != 0L) {
      val nextUp = classDumps[nextUpId]!!
      instanceSize += nextUp.fields.sumBy {
        typeSizes.getValue(it.type)
      }
      nextUpId = nextUp.superclassId
    }
    val classDump = ClassDumpRecord(
      id = loadClass.id,
      stackTraceSerialNumber = 1,
      superclassId = if (superclassId == -1L) objectClassId else superclassId,
      classLoaderId = 0,
      signersId = 0,
      protectionDomainId = 0,
      instanceSize = instanceSize,
      staticFields = staticFieldRecords,
      fields = fieldRecords
    )
    classDumps[loadClass.id] = classDump
    writer.write(classDump)
    val gcRoot = StickyClass(classDump.id)
    gcRoot(gcRoot)
    return classDump.id
  }

  fun stringRecord(
    name: String
  ): StringRecord {
    val stringRecord = StringRecord(id, name)
    writer.write(stringRecord)
    return stringRecord
  }

  fun clazz(
    classNameRecord: StringRecord,
    superclassId: Long = -1L, // -1 defaults to java.lang.Object
    staticFields: List<Pair<Long, ValueHolder>> = emptyList(),
    fields: List<Pair<Long, KClass<out ValueHolder>>> = emptyList()
  ): Long {
    val loadClass = LoadClassRecord(1, id, 1, classNameRecord.id)
    writer.write(loadClass)

    val staticFieldRecords = staticFields.map {
      StaticFieldRecord(it.first, typeOf(it.second), it.second)
    }

    val fieldRecords = fields.map {
      FieldRecord(it.first, typeOf(it.second))
    }

    var instanceSize = fieldRecords.sumBy {
      typeSizes.getValue(it.type)
    }

    var nextUpId = if (superclassId == -1L) objectClassId else superclassId
    while (nextUpId != 0L) {
      val nextUp = classDumps[nextUpId]!!
      instanceSize += nextUp.fields.sumBy {
        typeSizes.getValue(it.type)
      }
      nextUpId = nextUp.superclassId
    }
    val classDump = ClassDumpRecord(
      id = loadClass.id,
      stackTraceSerialNumber = 1,
      superclassId = if (superclassId == -1L) objectClassId else superclassId,
      classLoaderId = 0,
      signersId = 0,
      protectionDomainId = 0,
      instanceSize = instanceSize,
      staticFields = staticFieldRecords,
      fields = fieldRecords
    )
    classDumps[loadClass.id] = classDump
    writer.write(classDump)
    val gcRoot = StickyClass(classDump.id)
    gcRoot(gcRoot)
    return classDump.id
  }

  fun gcRoot(gcRoot: GcRoot) {
    val gcRootRecord = GcRootRecord(gcRoot = gcRoot)
    writer.write(gcRootRecord)
  }

  fun arrayClass(className: String): Long {
    return clazz(className = "$className[]")
  }

  fun string(
    string: String
  ): ReferenceHolder {
    return instance(
      stringClassId,
      fields = listOf(string.charArrayDump, IntHolder(string.length))
    )
  }

  fun keyedWeakReference(
    referentInstanceId: ReferenceHolder
  ): ReferenceHolder {
    val referenceKey = string(weakRefKey)
    return instance(
      classId = keyedWeakReferenceClassId,
      fields = listOf(
        referenceKey,
        string("its lifecycle has ended"),
        LongHolder(5000),
        LongHolder(20000),
        ReferenceHolder(referentInstanceId.value)
      )
    )
  }

  fun instance(
    classId: Long,
    fields: List<ValueHolder> = emptyList()
  ): ReferenceHolder {
    val instanceDump = InstanceDumpRecord(
      id = id,
      stackTraceSerialNumber = 1,
      classId = classId,
      fieldValues = writer.valuesToBytes(fields)
    )
    writer.write(instanceDump)
    return ReferenceHolder(instanceDump.id)
  }

  inner class InstanceAndClassDefinition {
    val field = LinkedHashMap<String, ValueHolder>()
    val staticField = LinkedHashMap<String, ValueHolder>()
  }

  inner class ClassDefinition {
    val staticField = LinkedHashMap<String, ValueHolder>()
  }

  infix fun String.watchedInstance(block: InstanceAndClassDefinition.() -> Unit): ReferenceHolder {
    val instance = this.instance(block)
    keyedWeakReference(instance)
    return instance
  }

  infix fun String.instance(block: InstanceAndClassDefinition.() -> Unit): ReferenceHolder {
    val definition = InstanceAndClassDefinition()
    block(definition)

    val classFields = definition.field.map {
      it.key to it.value::class
    }

    val staticFields = definition.staticField.map { it.key to it.value }

    val instanceFields = definition.field.map { it.value }

    return instance(clazz(this, fields = classFields, staticFields = staticFields), instanceFields)
  }

  infix fun String.clazz(block: ClassDefinition.() -> Unit): Long {
    val definition = ClassDefinition()
    block(definition)

    val staticFields = definition.staticField.map { it.key to it.value }
    return clazz(this, staticFields = staticFields)
  }

  val String.charArrayDump: ReferenceHolder
    get() {
      val arrayDump = CharArrayDump(id, 1, toCharArray())
      writer.write(arrayDump)
      return ReferenceHolder(arrayDump.id)
    }

  fun objectArray(
    vararg elements: ReferenceHolder
  ): ReferenceHolder {
    return objectArrayOf(objectArrayClassId, *elements)
  }

  fun objectArrayOf(
    classId: Long,
    vararg elements: ReferenceHolder
  ): ReferenceHolder {
    return ReferenceHolder(objectArray(classId, elements.map { it.value }.toLongArray()))
  }

  fun objectArray(
    classId: Long,
    array: LongArray
  ): Long {
    val arrayDump = ObjectArrayDumpRecord(id, 1, classId, array)
    writer.write(arrayDump)
    return arrayDump.id
  }

  fun primitiveLongArray(array: LongArray): Long {
    val arrayDump = LongArrayDump(id, 1, array)
    writer.write(arrayDump)
    return arrayDump.id
  }

  private fun typeOf(wrapper: ValueHolder): Int {
    return when (wrapper) {
      is ReferenceHolder -> PrimitiveType.REFERENCE_HPROF_TYPE
      is BooleanHolder -> BOOLEAN.hprofType
      is CharHolder -> CHAR.hprofType
      is FloatHolder -> FLOAT.hprofType
      is DoubleHolder -> DOUBLE.hprofType
      is ByteHolder -> BYTE.hprofType
      is ShortHolder -> SHORT.hprofType
      is IntHolder -> INT.hprofType
      is LongHolder -> LONG.hprofType
    }
  }

  private fun typeOf(wrapperClass: KClass<out ValueHolder>): Int {
    return when (wrapperClass) {
      ReferenceHolder::class -> PrimitiveType.REFERENCE_HPROF_TYPE
      BooleanHolder::class -> BOOLEAN.hprofType
      CharHolder::class -> CHAR.hprofType
      FloatHolder::class -> FLOAT.hprofType
      DoubleHolder::class -> DOUBLE.hprofType
      ByteHolder::class -> BYTE.hprofType
      ShortHolder::class -> SHORT.hprofType
      IntHolder::class -> INT.hprofType
      LongHolder::class -> LONG.hprofType
      else -> throw IllegalArgumentException("Unexpected class $wrapperClass")
    }
  }

  override fun close() {
    writer.close()
  }
}

fun File.dump(block: HprofWriterHelper.() -> Unit) {
  HprofWriterHelper(HprofWriter.openWriterFor(this))
    .use(block)
}

fun dump(
  hprofHeader: HprofHeader = HprofHeader(),
  block: HprofWriterHelper.() -> Unit
): DualSourceProvider {
  val buffer = Buffer()
  HprofWriterHelper(HprofWriter.openWriterFor(buffer, hprofHeader))
    .use(block)
  return ByteArraySourceProvider(buffer.readByteArray())
}

fun dumpToBytes(
  hprofHeader: HprofHeader = HprofHeader(),
  block: HprofWriterHelper.() -> Unit
): ByteArray {
  val buffer = Buffer()
  HprofWriterHelper(HprofWriter.openWriterFor(buffer, hprofHeader))
    .use(block)
  return buffer.readByteArray()
}

fun List<HprofRecord>.asHprofBytes(): DualSourceProvider {
  val buffer = Buffer()
  HprofWriter.openWriterFor(buffer)
    .use { writer ->
      forEach { record ->
        writer.write(record)
      }
    }
  return ByteArraySourceProvider(buffer.readByteArray())
}