All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.shark.AndroidMetadataExtractor.kt Maven / Gradle / Ivy

The newest version!
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 {
    val metadata = mutableMapOf()

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy