<lambda>null1 package shark
2
3 import com.github.ajalt.clikt.core.CliktCommand
4 import com.github.ajalt.clikt.core.PrintMessage
5 import jline.console.ConsoleReader
6 import jline.console.UserInterruptException
7 import jline.console.completer.CandidateListCompletionHandler
8 import jline.console.completer.StringsCompleter
9 import shark.HeapObject.HeapClass
10 import shark.HeapObject.HeapInstance
11 import shark.HeapObject.HeapObjectArray
12 import shark.HeapObject.HeapPrimitiveArray
13 import shark.HprofHeapGraph.Companion.openHeapGraph
14 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
15 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
16 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
17 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
18 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
19 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
20 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
21 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
22 import shark.InteractiveCommand.COMMAND.ANALYZE
23 import shark.InteractiveCommand.COMMAND.ARRAY
24 import shark.InteractiveCommand.COMMAND.CLASS
25 import shark.InteractiveCommand.COMMAND.Companion.matchesCommand
26 import shark.InteractiveCommand.COMMAND.DETAILED_PATH_TO_INSTANCE
27 import shark.InteractiveCommand.COMMAND.EXIT
28 import shark.InteractiveCommand.COMMAND.HELP
29 import shark.InteractiveCommand.COMMAND.INSTANCE
30 import shark.InteractiveCommand.COMMAND.PATH_TO_INSTANCE
31 import shark.SharkCliCommand.Companion.echoNewline
32 import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
33 import shark.SharkCliCommand.Companion.sharkCliParams
34 import shark.ValueHolder.BooleanHolder
35 import shark.ValueHolder.ByteHolder
36 import shark.ValueHolder.CharHolder
37 import shark.ValueHolder.DoubleHolder
38 import shark.ValueHolder.FloatHolder
39 import shark.ValueHolder.IntHolder
40 import shark.ValueHolder.LongHolder
41 import shark.ValueHolder.ReferenceHolder
42 import shark.ValueHolder.ShortHolder
43 import java.io.File
44 import java.util.Locale
45
46 class InteractiveCommand : CliktCommand(
47 name = "interactive",
48 help = "Explore a heap dump."
49 ) {
50
51 enum class COMMAND(
52 val commandName: String,
53 val suffix: String = "",
54 val help: String
55 ) {
56 ANALYZE(
57 commandName = "analyze",
58 help = "Analyze the heap dump."
59 ),
60 CLASS(
61 commandName = "class",
62 suffix = "NAME@ID",
63 help = "Show class with a matching NAME and Object ID."
64 ),
65 INSTANCE(
66 commandName = "instance",
67 suffix = "CLASS_NAME@ID",
68 help = "Show instance with a matching CLASS_NAME and Object ID."
69 ),
70 ARRAY(
71 commandName = "array",
72 suffix = "CLASS_NAME@ID",
73 help = "Show array instance with a matching CLASS_NAME and Object ID."
74 ),
75 PATH_TO_INSTANCE(
76 commandName = "->instance",
77 suffix = "CLASS_NAME@ID",
78 help = "Show path from GC Roots to instance."
79 ),
80 DETAILED_PATH_TO_INSTANCE(
81 commandName = "~>instance",
82 suffix = "CLASS_NAME@ID",
83 help = "Show path from GC Roots to instance, highlighting suspect references."
84 ),
85 HELP(
86 commandName = "help",
87 help = "Show this message."
88 ),
89 EXIT(
90 commandName = "exit",
91 help = "Exit this interactive prompt."
92 ),
93 ;
94
95 val pattern: String
96 get() = if (suffix.isEmpty()) commandName else "$commandName "
97
98 val patternHelp: String
99 get() = pattern + suffix
100
101 override fun toString() = commandName
102
103 companion object {
104 infix fun String.matchesCommand(command: COMMAND): Boolean {
105 return if (command.suffix.isEmpty()) {
106 this == command.commandName
107 } else {
108 startsWith(command.pattern)
109 }
110 }
111 }
112 }
113
114 override fun run() {
115 openHprof { graph, heapDumpFile ->
116 val console = setupConsole(graph)
117 var exit = false
118 while (!exit) {
119 val input = console.readCommand()
120 exit = handleCommand(input, heapDumpFile, graph)
121 echoNewline()
122 }
123 }
124 }
125
126 private fun openHprof(block: (HeapGraph, File) -> Unit) {
127 val params = context.sharkCliParams
128 val heapDumpFile = retrieveHeapDumpFile(params)
129 val obfuscationMappingPath = params.obfuscationMappingPath
130
131 val proguardMapping = obfuscationMappingPath?.let {
132 ProguardMappingReader(it.inputStream()).readProguardMapping()
133 }
134
135 heapDumpFile.openHeapGraph().use { graph ->
136 block(graph, heapDumpFile)
137 }
138 }
139
140 private fun setupConsole(graph: HeapGraph): ConsoleReader {
141 val console = ConsoleReader()
142 console.handleUserInterrupt = true
143
144 console.addCompleter(StringsCompleter(COMMAND.values().map { it.pattern }))
145 console.addCompleter { buffer, _, candidates ->
146 if (buffer != null) {
147 when {
148 buffer matchesCommand CLASS -> {
149 val matchingObjects = findMatchingObjects(buffer, graph.classes) {
150 it.name
151 }
152 candidates.addAll(matchingObjects.map { renderHeapObject(it) })
153 }
154 buffer matchesCommand INSTANCE -> {
155 val matchingObjects = findMatchingObjects(buffer, graph.instances) {
156 it.instanceClassSimpleName
157 }
158 candidates.addAll(matchingObjects.map { renderHeapObject(it) })
159 }
160 buffer matchesCommand PATH_TO_INSTANCE -> {
161 val matchingObjects = findMatchingObjects(buffer, graph.instances) {
162 it.instanceClassSimpleName
163 }
164 candidates.addAll(matchingObjects.map { "->${renderHeapObject(it)}" })
165 }
166 buffer matchesCommand DETAILED_PATH_TO_INSTANCE -> {
167 val matchingObjects = findMatchingObjects(buffer, graph.instances) {
168 it.instanceClassSimpleName
169 }
170 candidates.addAll(matchingObjects.map { "~>${renderHeapObject(it)}" })
171 }
172 buffer matchesCommand ARRAY -> {
173 val matchingObjects =
174 findMatchingObjects(buffer, graph.primitiveArrays + graph.objectArrays) {
175 if (it is HeapPrimitiveArray) {
176 it.arrayClassName
177 } else {
178 (it as HeapObjectArray).arrayClassSimpleName
179 }
180 }
181 candidates.addAll(matchingObjects.map { renderHeapObject(it) })
182 }
183 }
184 }
185 if (candidates.isEmpty()) -1 else 0
186 }
187 val completionHandler = CandidateListCompletionHandler()
188 completionHandler.printSpaceAfterFullCompletion = false
189 console.completionHandler = completionHandler
190 console.prompt = "Enter command [help]:\n"
191 return console
192 }
193
194 private fun ConsoleReader.readCommand(): String? {
195 val input = try {
196 readLine()
197 } catch (ignored: UserInterruptException) {
198 throw PrintMessage("Program interrupted by user")
199 }
200 echoNewline()
201 return input
202 }
203
204 private fun handleCommand(
205 input: String?,
206 heapDumpFile: File,
207 graph: HeapGraph
208 ): Boolean {
209 when {
210 input == null -> throw PrintMessage("End Of File was encountered")
211 input.isBlank() || input matchesCommand HELP -> echoHelp()
212 input matchesCommand EXIT -> return true
213 input matchesCommand ANALYZE -> analyze(heapDumpFile, graph)
214 input matchesCommand PATH_TO_INSTANCE -> {
215 analyzeMatchingObjects(heapDumpFile, input, graph.instances, false) {
216 it.instanceClassSimpleName
217 }
218 }
219 input matchesCommand DETAILED_PATH_TO_INSTANCE -> {
220 analyzeMatchingObjects(heapDumpFile, input, graph.instances, true) {
221 it.instanceClassSimpleName
222 }
223 }
224 input matchesCommand CLASS -> {
225 renderMatchingObjects(input, graph.classes) {
226 it.name
227 }
228 }
229 input matchesCommand INSTANCE -> {
230 renderMatchingObjects(input, graph.instances) {
231 it.instanceClassSimpleName
232 }
233 }
234 input matchesCommand ARRAY -> {
235 renderMatchingObjects(input, graph.primitiveArrays + graph.objectArrays) {
236 if (it is HeapPrimitiveArray) {
237 it.arrayClassName
238 } else {
239 (it as HeapObjectArray).arrayClassSimpleName
240 }
241 }
242 }
243 else -> {
244 echo("Unknown command [$input].\n")
245 echoHelp()
246 }
247 }
248 return false
249 }
250
251 private fun echoHelp() {
252 echo("Available commands:")
253 val longestPatternHelp = COMMAND.values()
254 .map { it.patternHelp }.maxBy { it.length }!!.length
255 COMMAND.values()
256 .forEach { command ->
257 val patternHelp = command.patternHelp
258 val extraSpaceCount = (longestPatternHelp - patternHelp.length)
259 val extraSpaces = " ".repeat(extraSpaceCount)
260 println(" $patternHelp$extraSpaces ${command.help}")
261 }
262 }
263
264 private fun <T : HeapObject> renderMatchingObjects(
265 pattern: String,
266 objects: Sequence<T>,
267 namer: (T) -> String
268 ) {
269 val matchingObjects = findMatchingObjects(pattern, objects, namer)
270 when {
271 matchingObjects.size == 1 -> {
272 matchingObjects.first()
273 .show()
274 }
275 matchingObjects.isNotEmpty() -> {
276 matchingObjects.forEach { heapObject ->
277 echo(renderHeapObject(heapObject))
278 }
279 }
280 else -> {
281 echo("No object found matching [$pattern]")
282 }
283 }
284 }
285
286 private fun <T : HeapObject> analyzeMatchingObjects(
287 heapDumpFile: File,
288 pattern: String,
289 objects: Sequence<T>,
290 showDetails: Boolean,
291 namer: (T) -> String
292 ) {
293 val matchingObjects = findMatchingObjects(pattern, objects, namer)
294 when {
295 matchingObjects.size == 1 -> {
296 val heapObject = matchingObjects.first()
297 analyze(heapDumpFile, heapObject.graph, showDetails, heapObject.objectId)
298 }
299 matchingObjects.isNotEmpty() -> {
300 matchingObjects.forEach { heapObject ->
301 echo(if (showDetails) "~>" else "->" + renderHeapObject(heapObject))
302 }
303 }
304 else -> {
305 echo("No object found matching [$pattern]")
306 }
307 }
308 }
309
310 private fun <T : HeapObject> findMatchingObjects(
311 pattern: String,
312 objects: Sequence<T>,
313 namer: (T) -> String
314 ): List<T> {
315 val firstSpaceIndex = pattern.indexOf(' ')
316 val contentStartIndex = firstSpaceIndex + 1
317 val nextSpaceIndex = pattern.indexOf(' ', contentStartIndex)
318 val endIndex = if (nextSpaceIndex != -1) nextSpaceIndex else pattern.length
319 val content = pattern.substring(contentStartIndex, endIndex)
320 val identifierIndex = content.indexOf('@')
321 val (classNamePart, objectIdStart) = if (identifierIndex == -1) {
322 content to null
323 } else {
324 content.substring(0, identifierIndex) to
325 content.substring(identifierIndex + 1)
326 }
327
328 val objectId = objectIdStart?.toLongOrNull()
329 val checkObjectId = objectId != null
330 val matchingObjects = objects
331 .filter {
332 classNamePart in namer(it) &&
333 (!checkObjectId ||
334 it.objectId.toString().startsWith(objectIdStart!!))
335 }
336 .toList()
337
338 if (objectIdStart != null) {
339 val exactMatchingByObjectId = matchingObjects.firstOrNull { objectId == it.objectId }
340 if (exactMatchingByObjectId != null) {
341 return listOf(exactMatchingByObjectId)
342 }
343 }
344
345 val exactMatchingByName = matchingObjects.filter { classNamePart == namer(it) }
346
347 return exactMatchingByName.ifEmpty {
348 matchingObjects
349 }
350 }
351
352 private fun HeapObject.show() {
353 when (this) {
354 is HeapInstance -> showInstance()
355 is HeapClass -> showClass()
356 is HeapObjectArray -> showObjectArray()
357 is HeapPrimitiveArray -> showPrimitiveArray()
358 }
359 }
360
361 private fun HeapInstance.showInstance() {
362 echo(renderHeapObject(this))
363 echo(" Instance of ${renderHeapObject(instanceClass)}")
364
365 val fieldsPerClass = readFields()
366 .toList()
367 .groupBy { it.declaringClass }
368 .toList()
369 .filter { it.first.name != "java.lang.Object" }
370 .reversed()
371
372 fieldsPerClass.forEach { (heapClass, fields) ->
373 echo(" Fields from ${renderHeapObject(heapClass)}")
374 fields.forEach { field ->
375 echo(" ${field.name} = ${renderHeapValue(field.value)}")
376 }
377 }
378 }
379
380 private fun HeapClass.showClass() {
381 echo(this@InteractiveCommand.renderHeapObject(this))
382 val superclass = superclass
383 if (superclass != null) {
384 echo(" Extends ${renderHeapObject(superclass)}")
385 }
386
387 val staticFields = readStaticFields()
388 .filter { field ->
389 !field.name.startsWith(
390 "\$class\$"
391 ) && field.name != "\$classOverhead"
392 }
393 .toList()
394 if (staticFields.isNotEmpty()) {
395 echo(" Static fields")
396 staticFields
397 .forEach { field ->
398 echo(" static ${field.name} = ${renderHeapValue(field.value)}")
399 }
400 }
401
402 val instances = when {
403 isPrimitiveArrayClass -> primitiveArrayInstances
404 isObjectArrayClass -> objectArrayInstances
405 else -> instances
406 }.toList()
407 if (instances.isNotEmpty()) {
408 echo(" ${instances.size} instance" + if (instances.size != 1) "s" else "")
409 instances.forEach { arrayOrInstance ->
410 echo(" ${renderHeapObject(arrayOrInstance)}")
411 }
412 }
413 }
414
415 private fun HeapObjectArray.showObjectArray() {
416 val elements = readElements()
417 echo(renderHeapObject(this))
418 echo(" Instance of ${renderHeapObject(arrayClass)}")
419 var repeatedValue: HeapValue? = null
420 var repeatStartIndex = 0
421 var lastIndex = 0
422 elements.forEachIndexed { index, element ->
423 lastIndex = index
424 if (repeatedValue == null) {
425 repeatedValue = element
426 repeatStartIndex = index
427 } else if (repeatedValue != element) {
428 val repeatEndIndex = index - 1
429 if (repeatStartIndex == repeatEndIndex) {
430 echo(" $repeatStartIndex = ${renderHeapValue(repeatedValue!!)}")
431 } else {
432 echo(" $repeatStartIndex..$repeatEndIndex = ${renderHeapValue(repeatedValue!!)}")
433 }
434 repeatedValue = element
435 repeatStartIndex = index
436 }
437 }
438 if (repeatedValue != null) {
439 if (repeatStartIndex == lastIndex) {
440 echo(" $repeatStartIndex = ${renderHeapValue(repeatedValue!!)}")
441 } else {
442 echo(" $repeatStartIndex..$lastIndex = ${renderHeapValue(repeatedValue!!)}")
443 }
444 }
445 }
446
447 private fun HeapPrimitiveArray.showPrimitiveArray() {
448 val record = readRecord()
449 echo(renderHeapObject(this))
450 echo(" Instance of ${renderHeapObject(arrayClass)}")
451
452 var repeatedValue: Any? = null
453 var repeatStartIndex = 0
454 var lastIndex = 0
455 val action: (Int, Any) -> Unit = { index, value ->
456 lastIndex = index
457 if (repeatedValue == null) {
458 repeatedValue = value
459 repeatStartIndex = index
460 } else if (repeatedValue != value) {
461 val repeatEndIndex = index - 1
462 if (repeatStartIndex == repeatEndIndex) {
463 echo(" $repeatStartIndex = $repeatedValue")
464 } else {
465 echo(" $repeatStartIndex..$repeatEndIndex = $repeatedValue")
466 }
467 repeatedValue = value
468 repeatStartIndex = index
469 }
470 }
471
472 when (record) {
473 is BooleanArrayDump -> record.array.forEachIndexed(action)
474 is CharArrayDump -> record.array.forEachIndexed(action)
475 is FloatArrayDump -> record.array.forEachIndexed(action)
476 is DoubleArrayDump -> record.array.forEachIndexed(action)
477 is ByteArrayDump -> record.array.forEachIndexed(action)
478 is ShortArrayDump -> record.array.forEachIndexed(action)
479 is IntArrayDump -> record.array.forEachIndexed(action)
480 is LongArrayDump -> record.array.forEachIndexed(action)
481 }
482 if (repeatedValue != null) {
483 if (repeatStartIndex == lastIndex) {
484 echo(" $repeatStartIndex = $repeatedValue")
485 } else {
486 echo(" $repeatStartIndex..$lastIndex = $repeatedValue")
487 }
488 }
489 }
490
491 private fun renderHeapValue(heapValue: HeapValue): String {
492 return when (val holder = heapValue.holder) {
493 is ReferenceHolder -> {
494 when {
495 holder.isNull -> "null"
496 !heapValue.graph.objectExists(holder.value) -> "@${holder.value} object not found"
497 else -> {
498 val heapObject = heapValue.asObject!!
499 renderHeapObject(heapObject)
500 }
501 }
502 }
503 is BooleanHolder -> holder.value.toString()
504 is CharHolder -> holder.value.toString()
505 is FloatHolder -> holder.value.toString()
506 is DoubleHolder -> holder.value.toString()
507 is ByteHolder -> holder.value.toString()
508 is ShortHolder -> holder.value.toString()
509 is IntHolder -> holder.value.toString()
510 is LongHolder -> holder.value.toString()
511 }
512 }
513
514 private fun renderHeapObject(heapObject: HeapObject): String {
515 return when (heapObject) {
516 is HeapClass -> {
517 val instanceCount = when {
518 heapObject.isPrimitiveArrayClass -> heapObject.primitiveArrayInstances
519 heapObject.isObjectArrayClass -> heapObject.objectArrayInstances
520 else -> heapObject.instances
521 }.count()
522 val plural = if (instanceCount != 1) "s" else ""
523 "$CLASS ${heapObject.name}@${heapObject.objectId} (${instanceCount} instance$plural)"
524 }
525 is HeapInstance -> {
526 val asJavaString = heapObject.readAsJavaString()
527
528 val value =
529 if (asJavaString != null) {
530 " \"${asJavaString}\""
531 } else ""
532
533 "$INSTANCE ${heapObject.instanceClassSimpleName}@${heapObject.objectId}$value"
534 }
535 is HeapObjectArray -> {
536 val className = heapObject.arrayClassSimpleName.removeSuffix("[]")
537 "$ARRAY $className[${heapObject.readElements().count()}]@${heapObject.objectId}"
538 }
539 is HeapPrimitiveArray -> {
540 val record = heapObject.readRecord()
541 val primitiveName = heapObject.primitiveType.name.toLowerCase(Locale.US)
542 "$ARRAY $primitiveName[${record.size}]@${heapObject.objectId}"
543 }
544 }
545 }
546
547 private fun analyze(
548 heapDumpFile: File,
549 graph: HeapGraph,
550 showDetails: Boolean = true,
551 leakingObjectId: Long? = null
552 ) {
553 if (leakingObjectId != null) {
554 if (!graph.objectExists(leakingObjectId)) {
555 echo("@$leakingObjectId not found")
556 return
557 } else {
558 val heapObject = graph.findObjectById(leakingObjectId)
559 if (heapObject !is HeapInstance) {
560 echo("${renderHeapObject(heapObject)} is not an instance")
561 return
562 }
563 }
564 }
565
566 val objectInspectors =
567 if (showDetails) AndroidObjectInspectors.appDefaults.toMutableList() else mutableListOf()
568
569 objectInspectors += ObjectInspector {
570 it.labels += renderHeapObject(it.heapObject)
571 }
572
573 val leakingObjectFinder = if (leakingObjectId == null) {
574 FilteringLeakingObjectFinder(
575 AndroidObjectInspectors.appLeakingObjectFilters
576 )
577 } else {
578 LeakingObjectFinder {
579 setOf(leakingObjectId)
580 }
581 }
582
583 val listener = OnAnalysisProgressListener { step ->
584 SharkLog.d { "Analysis in progress, working on: ${step.name}" }
585 }
586
587 val heapAnalyzer = HeapAnalyzer(listener)
588 SharkLog.d { "Analyzing heap dump $heapDumpFile" }
589
590 val heapAnalysis = heapAnalyzer.analyze(
591 heapDumpFile = heapDumpFile,
592 graph = graph,
593 leakingObjectFinder = leakingObjectFinder,
594 referenceMatchers = AndroidReferenceMatchers.appDefaults,
595 computeRetainedHeapSize = true,
596 objectInspectors = objectInspectors,
597 metadataExtractor = AndroidMetadataExtractor
598 )
599
600 if (leakingObjectId == null || heapAnalysis is HeapAnalysisFailure) {
601 echo(heapAnalysis)
602 } else {
603 val leakTrace = (heapAnalysis as HeapAnalysisSuccess).allLeaks.first()
604 .leakTraces.first()
605 echo(if (showDetails) leakTrace else leakTrace.toSimplePathString())
606 }
607 }
608 }
609