main

square/leakcanary

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

DumpProcessCommand.kt

TLDR

This file defines the DumpProcessCommand class, which is a command-line command that dumps the heap and pulls the hprof file. It also includes a utility method dumpHeap for dumping the heap of a specified process.

Methods

dumpHeap

This method takes two parameters: processNameParam (the name of the process to dump the heap for) and maybeDeviceId (optional device ID to specify the device to dump the heap from). It performs the following steps:

  1. Retrieves the working directory.
  2. Runs the adb devices command to get the list of connected devices.
  3. Filters and extracts the device IDs from the output of the adb devices command.
  4. If no devices are connected, throws a PrintMessage error.
  5. If no maybeDeviceId is specified and there is only one connected device, selects that device.
  6. If no maybeDeviceId is specified and there are multiple connected devices, throws a PrintMessage error.
  7. If maybeDeviceId is specified but it is not in the list of connected devices, throws a PrintMessage error.
  8. Runs the adb shell ps command to get the list of running processes on the specified device.
  9. Filters the processes to find the ones that contain processNameParam.
  10. If there is exactly one matching process, selects that process.
  11. If there are no matching processes, throws a PrintMessage error.
  12. If there are multiple matching processes but none matches exactly with processNameParam, throws a PrintMessage error.
  13. Generates a unique heap dump file name based on the current date and time.
  14. Specifies the path of the heap dump file on the device.
  15. Prints a message indicating the heap dump process has started.
  16. Runs the adb shell am dumpheap command to dump the heap for the selected process.
  17. Waits for 5 seconds.
  18. Prints a debug log message indicating that the heap dump is being pulled.
  19. Runs the adb pull command to pull the heap dump file from the device.
  20. Prints the output of the pull command.
  21. Prints a debug log message indicating that the heap dump file on the device is being removed.
  22. Runs the adb shell rm command to remove the heap dump file from the device.
  23. Creates a File object representing the pulled heap dump file.
  24. Prints a debug log message indicating the heap dump file has been pulled.
  25. Returns the File object representing the pulled heap dump file.

Classes

DumpProcessCommand

This class extends the CliktCommand class and represents a command-line command dump-process that dumps the heap and pulls the hprof file. It overrides the run method to perform the following steps:

  1. Retrieves the sharkCliParams from the command context.
  2. Throws a UsageError if the source in sharkCliParams is not a ProcessSource.
  3. Calls the retrieveHeapDumpFile method with sharkCliParams to retrieve the heap dump file.
  4. Prints a message indicating the heap dump file has been pulled.

That's all! There are no more methods or classes in this file.

package shark

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.PrintMessage
import com.github.ajalt.clikt.core.UsageError
import shark.SharkCliCommand.Companion.echo
import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
import shark.SharkCliCommand.Companion.runCommand
import shark.SharkCliCommand.Companion.sharkCliParams
import shark.SharkCliCommand.HeapDumpSource.ProcessSource
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class DumpProcessCommand : CliktCommand(
  name = "dump-process",
  help = "Dump the heap and pull the hprof file."
) {

  override fun run() {
    val params = context.sharkCliParams
    if (params.source !is ProcessSource) {
      throw UsageError("dump-process must be used with --process")
    }
    val file = retrieveHeapDumpFile(params)
    echo("Pulled heap dump to $file")
  }

  companion object {

    private val SPACE_PATTERN = Regex("\\s+")

    @Suppress("ThrowsCount")
    fun CliktCommand.dumpHeap(
      processNameParam: String,
      maybeDeviceId: String?
    ): File {
      val workingDirectory = File(System.getProperty("user.dir"))

      val deviceList = runCommand(workingDirectory, "adb", "devices")

      val connectedDevices = deviceList.lines()
        .drop(1)
        .filter { it.isNotBlank() }
        .map { SPACE_PATTERN.split(it)[0] }

      val deviceId = if (connectedDevices.isEmpty()) {
        throw PrintMessage("Error: No device connected to adb")
      } else if (maybeDeviceId == null) {
        if (connectedDevices.size == 1) {
          connectedDevices[0]
        } else {
          throw PrintMessage(
            "Error: more than one device/emulator connected to adb," +
              " use '--device ID' argument with one of $connectedDevices"
          )
        }
      } else {
        if (maybeDeviceId in connectedDevices) {
          maybeDeviceId
        } else {
          throw PrintMessage(
            "Error: device '$maybeDeviceId' not in the list of connected devices $connectedDevices"
          )
        }
      }

      val processList = runCommand(workingDirectory, "adb", "-s", deviceId, "shell", "ps")

      val matchingProcesses = processList.lines()
        .filter { it.contains(processNameParam) }
        .map {
          val columns = SPACE_PATTERN.split(it)
          columns[8] to columns[1]
        }

      val (processName, processId) = when {
        matchingProcesses.size == 1 -> {
          matchingProcesses[0]
        }
        matchingProcesses.isEmpty() -> {
          throw PrintMessage("Error: No process matching \"$processNameParam\"")
        }
        else -> {
          matchingProcesses.firstOrNull { it.first == processNameParam }
            ?: throw PrintMessage(
              "Error: More than one process matches \"$processNameParam\" but none matches exactly: ${matchingProcesses.map { it.first }}"
            )
        }
      }

      val heapDumpFileName =
        SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'-$processName.hprof'", Locale.US).format(
          Date()
        )

      val heapDumpDevicePath = "/data/local/tmp/$heapDumpFileName"

      echo(
        "Dumping heap on $deviceId for process \"$processName\" with pid $processId to $heapDumpDevicePath"
      )

      runCommand(
        workingDirectory, "adb", "-s", deviceId, "shell", "am", "dumpheap", processId,
        heapDumpDevicePath
      )

      // Dump heap takes time but adb returns immediately.
      Thread.sleep(5000)

      SharkLog.d { "Pulling $heapDumpDevicePath" }

      val pullResult =
        runCommand(workingDirectory, "adb", "-s", deviceId, "pull", heapDumpDevicePath)
      SharkLog.d { pullResult }
      SharkLog.d { "Removing $heapDumpDevicePath" }

      runCommand(workingDirectory, "adb", "-s", deviceId, "shell", "rm", heapDumpDevicePath)

      val heapDumpFile = File(workingDirectory, heapDumpFileName)
      SharkLog.d { "Pulled heap dump to $heapDumpFile" }

      return heapDumpFile
    }
  }
}