main

square/leakcanary

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

AndroidMetadataExtractor.kt

TLDR

This file, AndroidMetadataExtractor.kt, contains the implementation of the AndroidMetadataExtractor object. This object implements the MetadataExtractor interface and provides methods for extracting metadata from a heap graph.

Methods

extractMetadata

This method takes a HeapGraph as input and returns a Map containing various metadata about the heap graph. The metadata includes information such as the Android build version, manufacturer, LeakCanary version, process name, class count, instance count, thread count, heap total bytes, and more.

readHeapTotalBytes

This private method calculates and returns the total number of bytes occupied by all objects in the HeapGraph.

putBitmaps

This private method populates the metadata map with information about bitmaps present in the HeapGraph. It counts the number of bitmaps, calculates the total number of bytes occupied by bitmaps, counts the number of large bitmaps, and calculates the total number of bytes occupied by large bitmaps.

readThreadCount

This private method calculates and returns the total number of threads in the HeapGraph.

readLeakCanaryVersion

This private method retrieves the version of LeakCanary used in the HeapGraph, if available. If the version is not found, it returns "Unknown".

readProcessName

This private method retrieves the name of the process in which the HeapGraph was captured. If the process name is not found, it returns "Unknown".

putDbLabels

This private method populates the metadata map with information about open and closed database connections in the HeapGraph. It retrieves the label and open/close status of each database connection instance and adds it to the metadata map.

Classes

None

package shark

import shark.GcRoot.ThreadObject
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.internal.friendly.mapNativeSizes

object AndroidMetadataExtractor : MetadataExtractor {
  override fun extractMetadata(graph: HeapGraph): Map<String, String> {
    val metadata = mutableMapOf<String, String>()

    val build = AndroidBuildMirror.fromHeapGraph(graph)
    metadata["Build.VERSION.SDK_INT"] = build.sdkInt.toString()
    metadata["Build.MANUFACTURER"] = build.manufacturer
    metadata["LeakCanary version"] = readLeakCanaryVersion(graph)
    metadata["App process name"] = readProcessName(graph)
    metadata["Class count"] = graph.classCount.toString()
    metadata["Instance count"] = graph.instanceCount.toString()
    metadata["Primitive array count"] = graph.primitiveArrayCount.toString()
    metadata["Object array count"] = graph.objectArrayCount.toString()
    metadata["Thread count"] = readThreadCount(graph).toString()
    metadata["Heap total bytes"] = readHeapTotalBytes(graph).toString()
    metadata.putBitmaps(graph)
    metadata.putDbLabels(graph)

    return metadata
  }

  private fun readHeapTotalBytes(graph: HeapGraph): Int {
    return graph.objects.sumBy { heapObject ->
      when(heapObject) {
        is HeapInstance -> {
          heapObject.byteSize
        }
        // This is probably way off but is a cheap approximation.
        is HeapClass -> heapObject.recordSize
        is HeapObjectArray -> heapObject.byteSize
        is HeapPrimitiveArray -> heapObject.byteSize
      }
    }
  }

  private fun MutableMap<String, String>.putBitmaps(
    graph: HeapGraph,
  ) {

    val bitmapClass = graph.findClassByName("android.graphics.Bitmap") ?: return

    val maxDisplayPixels =
      graph.findClassByName("android.util.DisplayMetrics")?.directInstances?.map { instance ->
        val width = instance["android.util.DisplayMetrics", "widthPixels"]?.value?.asInt ?: 0
        val height = instance["android.util.DisplayMetrics", "heightPixels"]?.value?.asInt ?: 0
        width * height
      }?.max() ?: 0

    val maxDisplayPixelsWithThreshold = (maxDisplayPixels * 1.1).toInt()

    val sizeMap = graph.mapNativeSizes()

    var sizeSum = 0
    var count = 0
    var largeBitmapCount = 0
    var largeBitmapSizeSum = 0
    bitmapClass.instances.forEach { bitmap ->
      val width = bitmap["android.graphics.Bitmap", "mWidth"]?.value?.asInt ?: 0
      val height = bitmap["android.graphics.Bitmap", "mHeight"]?.value?.asInt ?: 0
      val size = sizeMap[bitmap.objectId] ?: 0

      count++
      sizeSum += size
      if (maxDisplayPixelsWithThreshold > 0 && width * height > maxDisplayPixelsWithThreshold) {
        largeBitmapCount++
        largeBitmapSizeSum += size
      }
    }
    this["Bitmap count"] = count.toString()
    this["Bitmap total bytes"] = sizeSum.toString()
    this["Large bitmap count"] = largeBitmapCount.toString()
    this["Large bitmap total bytes"] = largeBitmapSizeSum.toString()
  }

  private fun readThreadCount(graph: HeapGraph): Int {
    return graph.gcRoots.filterIsInstance<ThreadObject>().map { it.id }.toSet().size
  }

  private fun readLeakCanaryVersion(graph: HeapGraph): String {
    val versionHolderClass = graph.findClassByName("leakcanary.internal.InternalLeakCanary")
    return versionHolderClass?.get("version")?.value?.readAsJavaString() ?: "Unknown"
  }

  private fun readProcessName(graph: HeapGraph): String {
    val activityThread = graph.findClassByName("android.app.ActivityThread")
      ?.get("sCurrentActivityThread")
      ?.valueAsInstance
    val appBindData = activityThread?.get("android.app.ActivityThread", "mBoundApplication")
      ?.valueAsInstance
    val appInfo = appBindData?.get("android.app.ActivityThread\$AppBindData", "appInfo")
      ?.valueAsInstance

    return appInfo?.get(
      "android.content.pm.ApplicationInfo", "processName"
    )?.valueAsInstance?.readAsJavaString() ?: "Unknown"
  }

  private fun MutableMap<String, String>.putDbLabels(graph: HeapGraph) {
    val dbClass = graph.findClassByName("android.database.sqlite.SQLiteDatabase") ?: return

    val openDbLabels = dbClass.instances.mapNotNull { instance ->
      val config =
        instance["android.database.sqlite.SQLiteDatabase", "mConfigurationLocked"]?.valueAsInstance
          ?: return@mapNotNull null
      val label =
        config["android.database.sqlite.SQLiteDatabaseConfiguration", "label"]?.value?.readAsJavaString()
          ?: return@mapNotNull null
      val open =
        instance["android.database.sqlite.SQLiteDatabase", "mConnectionPoolLocked"]?.value?.isNonNullReference
          ?: return@mapNotNull null
      label to open
    }

    openDbLabels.forEachIndexed { index, (label, open) ->
      this["Db ${index + 1}"] = (if (open) "open " else "closed ") + label
    }
  }
}