main

square/leakcanary

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

HeapAnalysisTable.kt

TLDR

This file, HeapAnalysisTable.kt, contains an object named HeapAnalysisTable which has various methods for manipulating the heap_analysis table in a SQLite database. The methods include inserting new data, retrieving data, and deleting data from the table.

Methods

onUpdate

A method that registers a listener that will be called whenever an update is made to the heap_analysis table. It returns a function that can be called to remove the listener.

insert

Inserts a new HeapAnalysis object into the heap_analysis table. The method takes an instance of SQLiteDatabase and a HeapAnalysis object as parameters.

notifyUpdateOnMainThread

Notifies all registered update listeners on the main thread that an update has been made to the heap_analysis table.

retrieve

Retrieves a HeapAnalysis object from the heap_analysis table based on the specified id. The method takes an instance of SQLiteDatabase and the id as parameters. It returns a nullable type based on the type parameter provided.

retrieveAll

Retrieves a list of Projection objects representing summaries of all rows in the heap_analysis table. The method takes an instance of SQLiteDatabase as a parameter.

delete

Deletes a row from the heap_analysis table based on the specified heapAnalysisId. The method also deletes the corresponding heap dump file if provided.

deleteAll

Deletes all rows from the heap_analysis table. The method takes an instance of SQLiteDatabase as a parameter.

Classes

No classes are defined in this file.

package leakcanary.internal.activity.db

import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.os.AsyncTask
import leakcanary.internal.LeakDirectoryProvider
import leakcanary.internal.Serializables
import leakcanary.internal.toByteArray
import leakcanary.internal.friendly.checkNotMainThread
import leakcanary.internal.friendly.mainHandler
import org.intellij.lang.annotations.Language
import shark.HeapAnalysis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.SharkLog
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList

internal object HeapAnalysisTable {

  /**
   * CopyOnWriteArrayList because registered listeners can remove themselves from this list while
   * iterating and invoking them, which would trigger a ConcurrentModificationException (see #2019).
   */
  private val updateListeners = CopyOnWriteArrayList<() -> Unit>()

  @Language("RoomSql")
  const val create = """CREATE TABLE heap_analysis
        (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        created_at_time_millis INTEGER,
        dump_duration_millis INTEGER DEFAULT -1,
        leak_count INTEGER DEFAULT 0,
        exception_summary TEXT DEFAULT NULL,
        object BLOB
        )"""

  @Language("RoomSql")
  const val drop = "DROP TABLE IF EXISTS heap_analysis"

  fun onUpdate(block: () -> Unit): () -> Unit {
    updateListeners.add(block)
    return {
      updateListeners.remove(block)
    }
  }

  fun insert(
    db: SQLiteDatabase,
    heapAnalysis: HeapAnalysis
  ): Long {
    val values = ContentValues()
    values.put("created_at_time_millis", heapAnalysis.createdAtTimeMillis)
    values.put("dump_duration_millis", heapAnalysis.dumpDurationMillis)
    values.put("object", heapAnalysis.toByteArray())
    when (heapAnalysis) {
      is HeapAnalysisSuccess -> {
        val leakCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size
        values.put("leak_count", leakCount)
      }
      is HeapAnalysisFailure -> {
        val cause = heapAnalysis.exception.cause!!
        val exceptionSummary = "${cause.javaClass.simpleName} ${cause.message}"
        values.put("exception_summary", exceptionSummary)
      }
    }

    return db.inTransaction {
      val heapAnalysisId = db.insertOrThrow("heap_analysis", null, values)
      if (heapAnalysis is HeapAnalysisSuccess) {
        heapAnalysis.allLeaks
          .forEach { leakingInstance ->
            LeakTable.insert(
              db, heapAnalysisId, leakingInstance
            )
          }
      }
      heapAnalysisId
    }.apply { notifyUpdateOnMainThread() }
  }

  private fun notifyUpdateOnMainThread() {
    checkNotMainThread()
    mainHandler.post {
      updateListeners.forEach { it() }
    }
  }

  inline fun <reified T : HeapAnalysis> retrieve(
    db: SQLiteDatabase,
    id: Long
  ): T? {
    return db.rawQuery(
      """
              SELECT
              object
              FROM heap_analysis
              WHERE id=$id
              """, null
    )
      .use { cursor ->
        if (cursor.moveToNext()) {
          val analysis = Serializables.fromByteArray<T>(cursor.getBlob(0))
          if (analysis == null) {
            delete(db, id, null)
          }
          analysis
        } else {
          null
        }
      }
  }

  fun retrieveAll(db: SQLiteDatabase): List<Projection> {
    return db.rawQuery(
      """
          SELECT
          id
          , created_at_time_millis
          , leak_count
          , exception_summary
          FROM heap_analysis
          ORDER BY created_at_time_millis DESC
          """, null
    )
      .use { cursor ->
        val all = mutableListOf<Projection>()
        while (cursor.moveToNext()) {
          val summary = Projection(
            id = cursor.getLong(0),
            createdAtTimeMillis = cursor.getLong(1),
            leakCount = cursor.getInt(2),
            exceptionSummary = cursor.getString(3)
          )
          all.add(summary)
        }
        all
      }
  }

  fun delete(
    db: SQLiteDatabase,
    heapAnalysisId: Long,
    heapDumpFile: File?
  ) {
    if (heapDumpFile != null) {
      AsyncTask.SERIAL_EXECUTOR.execute {
        val path = heapDumpFile.absolutePath
        val heapDumpDeleted = heapDumpFile.delete()
        if (heapDumpDeleted) {
          LeakDirectoryProvider.filesDeletedRemoveLeak += path
        } else {
          SharkLog.d { "Could not delete heap dump file ${heapDumpFile.path}" }
        }
      }
    }

    db.inTransaction {
      db.delete("heap_analysis", "id=$heapAnalysisId", null)
      LeakTable.deleteByHeapAnalysisId(db, heapAnalysisId)
    }
    notifyUpdateOnMainThread()
  }

  fun deleteAll(db: SQLiteDatabase) {
    db.inTransaction {
      rawQuery(
        """
              SELECT
              id,
              object
              FROM heap_analysis
              """, null
      )
        .use { cursor ->
          val all = mutableListOf<Pair<Long, HeapAnalysis>>()
          while (cursor.moveToNext()) {
            val id = cursor.getLong(0)
            val analysis = Serializables.fromByteArray<HeapAnalysis>(cursor.getBlob(1))
            if (analysis != null) {
              all += id to analysis
            }
          }
          all.forEach { (id, _) ->
            db.delete("heap_analysis", "id=$id", null)
            LeakTable.deleteByHeapAnalysisId(db, id)
          }
          AsyncTask.SERIAL_EXECUTOR.execute {
            all.forEach { (_, analysis) ->
              analysis.heapDumpFile.delete()
            }
          }
        }
    }
    notifyUpdateOnMainThread()
  }

  class Projection(
    val id: Long,
    val createdAtTimeMillis: Long,
    val leakCount: Int,
    val exceptionSummary: String?
  )
}