• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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