blob: 2736fa4f46dac19d74fda484728322f229dc843a [file] [log] [blame]
package shark
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.PrintMessage
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.split
import com.github.ajalt.clikt.parameters.options.validate
import com.github.ajalt.clikt.parameters.types.int
import jline.console.ConsoleReader
import jline.console.UserInterruptException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
import shark.DumpProcessCommand.Companion.dumpHeap
import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.SharkCliCommand.Companion.sharkCliParams
import shark.SharkCliCommand.HeapDumpSource.HprofDirectorySource
import shark.SharkCliCommand.HeapDumpSource.HprofFileSource
import shark.SharkCliCommand.HeapDumpSource.ProcessSource
class HeapGrowthCommand : CliktCommand(
name = "heap-growth",
help = "Detect heap growth"
) {
private val scenarioLoopsPerDump by option(
"--loops", "-l",
help = "The number of scenario iteration in between each heap dump."
).int().default(1).validate { if (it <= 0) fail("$it is not greater than 0") }
private val ignoredFields by option("--ignored-fields")
.split(",")
private val ignoredStaticFields by option("--ignored-static-fields")
.split(",")
override fun run() {
val params = context.sharkCliParams
val ignoredInstanceFieldReferences = ignoredFields?.let { ignoredFields ->
ignoredFields.map { ignoredField ->
val className = ignoredField.substringBeforeLast(".")
val fieldName = ignoredField.substringAfterLast(".")
ReferencePattern.instanceField(className, fieldName).ignored()
}
} ?: emptyList()
val ignoredStaticFieldReferences = ignoredStaticFields?.let { ignoredStaticFields ->
ignoredStaticFields.map { ignoredStaticField ->
val className = ignoredStaticField.substringBeforeLast(".")
val fieldName = ignoredStaticField.substringAfterLast(".")
ReferencePattern.staticField(className, fieldName).ignored()
}
} ?: emptyList()
val referenceMatchers = AndroidObjectGrowthReferenceMatchers.defaults +
ignoredInstanceFieldReferences +
ignoredStaticFieldReferences
val androidDetector = ObjectGrowthDetector
.forAndroidHeap(referenceMatchers)
data class Metrics(
val randomAccessByteReads: Long,
val randomAccessReadCount: Long,
val duration: Duration
)
val metrics = mutableListOf<Metrics>()
when (val source = params.source) {
is HprofFileSource -> throw CliktError(
"$commandName requires passing in a directory containing more than one hprof files."
)
is HprofDirectorySource -> {
val hprofFiles = source.hprofFiles.sortedBy { it.name }
if (hprofFiles.size == 1) {
throw CliktError(
"$commandName requires passing in a directory containing more than one hprof " +
"files, could only find ${hprofFiles.first().name} in " +
source.directory.absolutePath
)
}
echo(
"Detecting heap growth by going analyzing the following heap dumps in this " +
"order:\n${hprofFiles.joinToString("\n") { it.name }}"
)
var lastTraversal: HeapTraversalInput = InitialState(scenarioLoopsPerDump)
for (hprofFile in hprofFiles) {
val start = System.nanoTime().nanoseconds
val sourceProvider =
ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(hprofFile))
val heapTraversal = sourceProvider.openHeapGraph().use { heapGraph ->
androidDetector.findGrowingObjects(
heapGraph = heapGraph,
previousTraversal = lastTraversal,
)
}
val duration = System.nanoTime().nanoseconds - start
metrics += Metrics(
sourceProvider.randomAccessByteReads, sourceProvider.randomAccessReadCount, duration
)
lastTraversal = heapTraversal
if (heapTraversal is HeapDiff && !heapTraversal.isGrowing) {
break
}
}
val heapDiff = lastTraversal as HeapDiff
if (heapDiff.isGrowing) {
echo("Results: $heapDiff")
echo("Found ${heapDiff.growingObjects.size} growing objects")
} else {
echo("Results: not growing")
}
}
is ProcessSource -> {
echo("Detecting heap growth live")
ConsoleReader().readLine("Press ENTER to dump heap when ready to start")
val firstTraversal = source.dumpHeapAndOpenGraph().use { heapGraph ->
androidDetector.findGrowingObjects(
heapGraph = heapGraph,
previousTraversal = InitialState(scenarioLoopsPerDump)
)
}
val nTimes = if (scenarioLoopsPerDump > 1) "$scenarioLoopsPerDump times" else "once"
ConsoleReader().readLine("Go through scenario $nTimes then press ENTER to dump heap")
var latestTraversal = androidDetector.findGrowingObjects(
heapGraph = source.dumpHeapAndOpenGraph(),
previousTraversal = firstTraversal
) as HeapDiff
while (true) {
echo("Results: $latestTraversal")
echo(
"Found ${latestTraversal.growingObjects.size} objects growing over the last ${latestTraversal.traversalCount} heap dumps."
)
val consoleReader = ConsoleReader()
var reset = false
var promptForCommand = true
while (promptForCommand) {
if (latestTraversal.isGrowing) {
echo("To keep going, go through scenario $nTimes.")
echo(
"Then, either press ENTER or enter 'r' to reset and use the last heap dump as the new baseline."
)
echo("To quit, enter 'q'.")
val command = consoleReader.readCommand(
)
when (command) {
"q" -> throw PrintMessage("Quitting.")
"r" -> {
reset = true
promptForCommand = false
}
"" -> promptForCommand = false
else -> {
echo("Invalid command '$command'")
}
}
} else {
echo(
"As the last heap dump found 0 growing objects, there's no point continuing with the same heap dump baseline."
)
echo(
"To keep going, go through scenario $nTimes then press ENTER to use the last heap dump as the NEW baseline."
)
echo("To quit, enter 'q'.")
when (val command = consoleReader.readCommand()) {
"q" -> throw PrintMessage("Quitting.")
"" -> {
promptForCommand = false
reset = true
}
else -> {
echo("Invalid command '$command'")
}
}
}
}
val nextInputTraversal = if (reset) {
FirstHeapTraversal(
shortestPathTree = latestTraversal.shortestPathTree.copyResettingAsInitialTree(),
previousTraversal = InitialState(latestTraversal.scenarioLoopsPerGraph)
)
} else {
latestTraversal
}
latestTraversal = androidDetector.findGrowingObjects(
heapGraph = source.dumpHeapAndOpenGraph(),
previousTraversal = nextInputTraversal
) as HeapDiff
}
}
}
SharkLog.d { "Metrics:\n${metrics.joinToString("\n")}" }
}
private fun ConsoleReader.readCommand(): String? {
val input = try {
readLine()
} catch (ignored: UserInterruptException) {
throw PrintMessage("Program interrupted by user")
}
return input
}
private fun ProcessSource.dumpHeapAndOpenGraph(): CloseableHeapGraph =
dumpHeap(processName, deviceId).openHeapGraph()
}