Profiler.kt
TLDR
The Profiler.kt
file in the leakcanary
package contains an object called Profiler
, which provides helper methods for working with Android Studio's Profiler. It provides methods for waiting until CPU sampling starts and stops, for running a block of code with CPU sampling, and for running a block of code with method tracing to the SD card. It also provides utility methods for finding a specific thread by name.
Methods
waitForSamplingStart
This method waits until the Profiler is attached and CPU sampling is started. It will sleep until a sampling thread exists, and then sleep for an additional period of time to ensure the profiler has started sampling.
waitForSamplingStop
This method waits until CPU sampling stops. It will sleep until there is no sampling thread.
runWithProfilerSampling
This generic method executes the given function with CPU sampling via the Profiler and returns the result of the function execution. It first waits for the Profiler to be attached at the start of sampling, then executes the provided function block, and finally waits for sampling to stop.
runWithMethodTracing
This generic method executes the given function with method tracing to the SD card and returns the result of the function execution. Tracing is performed with Debug.startMethodTracingSampling
which uses sampling with a specified interval. The trace file will be stored in a specified folder on the SD card and can be pulled using the adb pull
command.
sleepUntil
This private inline function sleeps until the provided condition is true. It repeatedly sleeps for a fixed period of time until the condition is met.
samplingThreadExists
This private function checks if a sampling thread with a specific name exists. It calls the findThread
method to find the thread.
findThread
This private function finds a thread by its name among all active threads. It starts from the root thread group and iterates through all threads until it finds a match.
package leakcanary
import android.os.Debug
import shark.SharkLog
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Helper class for working with Android Studio's Profiler
*/
internal object Profiler {
private const val SLEEP_TIME_MILLIS = 1000L
private const val SAMPLING_THREAD_NAME = "Sampling Profiler"
/**
* Wait until Profiler is attached and CPU Sampling is started.
* Calling this on main thread can lead to ANR if you try to interact with UI while it's waiting for
* profiler.
* Note: only works with 'Sample Java Methods' profiling, won't work with 'Trace Java Methods'!
*/
fun waitForSamplingStart() {
SharkLog.d { "Waiting for sampling to start. Go to Profiler -> CPU -> Record" }
sleepUntil { samplingThreadExists() }
Thread.sleep(SLEEP_TIME_MILLIS) //Wait a bit more to ensure profiler started sampling
SharkLog.d { "Sampling started! Proceeding..." }
}
/**
* Wait until CPU Sampling stops.
* Calling this on main thread can lead to ANR if you try to interact with UI while it's waiting for
* profiler.
*/
fun waitForSamplingStop() {
SharkLog.d { "Waiting for sampling to stop. Go to Profiler -> CPU -> Stop recording" }
sleepUntil { !samplingThreadExists() }
SharkLog.d { "Sampling stopped! Proceeding..." }
}
/**
* Executes the given function [block] with CPU sampling via Profiler and returns the result of
* the function execution.
* First, it awaits for Profiler to be attached at start of sampling, then executes [block]
* and finally waits for sampling to stop. See [waitForSamplingStart] and [waitForSamplingStop]
* for more details.
*/
fun <T> runWithProfilerSampling(block: () -> T): T {
waitForSamplingStart()
val result = block()
waitForSamplingStop()
return result
}
private const val TRACES_FOLDER = "/sdcard/traces/"
private const val TRACE_NAME_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS'.trace'"
private const val BUFFER_SIZE = 50 * 1024 * 1024
private const val TRACE_INTERVAL_US = 1000
/**
* Executes the given function [block] with method tracing to SD card and returns the result of
* the function execution.
* Tracing is performed with [Debug.startMethodTracingSampling] which uses sampling with
* [TRACE_INTERVAL_US] microseconds interval.
* Trace file will be stored in [TRACES_FOLDER] and can be pulled via `adb pull` command.
* See Logcat output for an exact command to retrieve trace file
*/
fun <T> runWithMethodTracing(block: () -> T): T {
java.io.File(TRACES_FOLDER).mkdirs()
val fileName = SimpleDateFormat(TRACE_NAME_PATTERN, Locale.US).format(Date())
Debug.startMethodTracingSampling(
"$TRACES_FOLDER$fileName",
BUFFER_SIZE,
TRACE_INTERVAL_US
)
val result = block()
Debug.stopMethodTracing()
SharkLog.d { "Method tracing complete! Run the following command to retrieve the trace:" }
SharkLog.d { "adb pull $TRACES_FOLDER$fileName ~/Downloads/ " }
return result
}
private inline fun sleepUntil(condition: () -> Boolean) {
while (true) {
if (condition()) return else Thread.sleep(SLEEP_TIME_MILLIS)
}
}
private fun samplingThreadExists() = findThread(SAMPLING_THREAD_NAME) != null
/**
* Utility to get thread by its name; in case of multiple matches first one will be returned.
*/
private fun findThread(threadName: String): Thread? {
// Based on https://stackoverflow.com/a/1323480
var rootGroup = Thread.currentThread().threadGroup
while (rootGroup.parent != null) rootGroup = rootGroup.parent
var threads = arrayOfNulls<Thread>(rootGroup.activeCount())
while (rootGroup.enumerate(threads, true) == threads.size) {
threads = arrayOfNulls(threads.size * 2)
}
return threads.firstOrNull { it?.name == threadName }
}
}