1 package shark 2 3 import com.github.ajalt.clikt.core.CliktCommand 4 import com.github.ajalt.clikt.core.PrintMessage 5 import com.github.ajalt.clikt.core.UsageError 6 import shark.SharkCliCommand.Companion.echo 7 import shark.SharkCliCommand.Companion.retrieveHeapDumpFile 8 import shark.SharkCliCommand.Companion.runCommand 9 import shark.SharkCliCommand.Companion.sharkCliParams 10 import shark.SharkCliCommand.HeapDumpSource.ProcessSource 11 import java.io.File 12 import java.text.SimpleDateFormat 13 import java.util.Date 14 import java.util.Locale 15 16 class DumpProcessCommand : CliktCommand( 17 name = "dump-process", 18 help = "Dump the heap and pull the hprof file." 19 ) { 20 runnull21 override fun run() { 22 val params = context.sharkCliParams 23 if (params.source !is ProcessSource) { 24 throw UsageError("dump-process must be used with --process") 25 } 26 val file = retrieveHeapDumpFile(params) 27 echo("Pulled heap dump to $file") 28 } 29 30 companion object { 31 32 private val SPACE_PATTERN = Regex("\\s+") 33 34 @Suppress("ThrowsCount") CliktCommandnull35 fun CliktCommand.dumpHeap( 36 processNameParam: String, 37 maybeDeviceId: String? 38 ): File { 39 val workingDirectory = File(System.getProperty("user.dir")) 40 41 val deviceList = runCommand(workingDirectory, "adb", "devices") 42 43 val connectedDevices = deviceList.lines() 44 .drop(1) 45 .filter { it.isNotBlank() } 46 .map { SPACE_PATTERN.split(it)[0] } 47 48 val deviceId = if (connectedDevices.isEmpty()) { 49 throw PrintMessage("Error: No device connected to adb") 50 } else if (maybeDeviceId == null) { 51 if (connectedDevices.size == 1) { 52 connectedDevices[0] 53 } else { 54 throw PrintMessage( 55 "Error: more than one device/emulator connected to adb," + 56 " use '--device ID' argument with one of $connectedDevices" 57 ) 58 } 59 } else { 60 if (maybeDeviceId in connectedDevices) { 61 maybeDeviceId 62 } else { 63 throw PrintMessage( 64 "Error: device '$maybeDeviceId' not in the list of connected devices $connectedDevices" 65 ) 66 } 67 } 68 69 val processList = runCommand(workingDirectory, "adb", "-s", deviceId, "shell", "ps") 70 71 val matchingProcesses = processList.lines() 72 .filter { it.contains(processNameParam) } 73 .map { 74 val columns = SPACE_PATTERN.split(it) 75 columns[8] to columns[1] 76 } 77 78 val (processName, processId) = when { 79 matchingProcesses.size == 1 -> { 80 matchingProcesses[0] 81 } 82 matchingProcesses.isEmpty() -> { 83 throw PrintMessage("Error: No process matching \"$processNameParam\"") 84 } 85 else -> { 86 matchingProcesses.firstOrNull { it.first == processNameParam } 87 ?: throw PrintMessage( 88 "Error: More than one process matches \"$processNameParam\" but none matches exactly: ${matchingProcesses.map { it.first }}" 89 ) 90 } 91 } 92 93 val heapDumpFileName = 94 SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'-$processName.hprof'", Locale.US).format( 95 Date() 96 ) 97 98 val heapDumpDevicePath = "/data/local/tmp/$heapDumpFileName" 99 100 echo( 101 "Dumping heap on $deviceId for process \"$processName\" with pid $processId to $heapDumpDevicePath" 102 ) 103 104 runCommand( 105 workingDirectory, "adb", "-s", deviceId, "shell", "am", "dumpheap", processId, 106 heapDumpDevicePath 107 ) 108 109 // Dump heap takes time but adb returns immediately. 110 Thread.sleep(5000) 111 112 SharkLog.d { "Pulling $heapDumpDevicePath" } 113 114 val pullResult = 115 runCommand(workingDirectory, "adb", "-s", deviceId, "pull", heapDumpDevicePath) 116 SharkLog.d { pullResult } 117 SharkLog.d { "Removing $heapDumpDevicePath" } 118 119 runCommand(workingDirectory, "adb", "-s", deviceId, "shell", "rm", heapDumpDevicePath) 120 121 val heapDumpFile = File(workingDirectory, heapDumpFileName) 122 SharkLog.d { "Pulled heap dump to $heapDumpFile" } 123 124 return heapDumpFile 125 } 126 } 127 }