1 package leakcanary 2 3 import android.os.Debug 4 import shark.SharkLog 5 import java.text.SimpleDateFormat 6 import java.util.Date 7 import java.util.Locale 8 9 /** 10 * Helper class for working with Android Studio's Profiler 11 */ 12 internal object Profiler { 13 private const val SLEEP_TIME_MILLIS = 1000L 14 private const val SAMPLING_THREAD_NAME = "Sampling Profiler" 15 16 /** 17 * Wait until Profiler is attached and CPU Sampling is started. 18 * Calling this on main thread can lead to ANR if you try to interact with UI while it's waiting for 19 * profiler. 20 * Note: only works with 'Sample Java Methods' profiling, won't work with 'Trace Java Methods'! 21 */ waitForSamplingStartnull22 fun waitForSamplingStart() { 23 SharkLog.d { "Waiting for sampling to start. Go to Profiler -> CPU -> Record" } 24 sleepUntil { samplingThreadExists() } 25 Thread.sleep(SLEEP_TIME_MILLIS) //Wait a bit more to ensure profiler started sampling 26 SharkLog.d { "Sampling started! Proceeding..." } 27 } 28 29 /** 30 * Wait until CPU Sampling stops. 31 * Calling this on main thread can lead to ANR if you try to interact with UI while it's waiting for 32 * profiler. 33 */ waitForSamplingStopnull34 fun waitForSamplingStop() { 35 SharkLog.d { "Waiting for sampling to stop. Go to Profiler -> CPU -> Stop recording" } 36 sleepUntil { !samplingThreadExists() } 37 SharkLog.d { "Sampling stopped! Proceeding..." } 38 } 39 40 /** 41 * Executes the given function [block] with CPU sampling via Profiler and returns the result of 42 * the function execution. 43 * First, it awaits for Profiler to be attached at start of sampling, then executes [block] 44 * and finally waits for sampling to stop. See [waitForSamplingStart] and [waitForSamplingStop] 45 * for more details. 46 */ runWithProfilerSamplingnull47 fun <T> runWithProfilerSampling(block: () -> T): T { 48 waitForSamplingStart() 49 val result = block() 50 waitForSamplingStop() 51 return result 52 } 53 54 private const val TRACES_FOLDER = "/sdcard/traces/" 55 private const val TRACE_NAME_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS'.trace'" 56 private const val BUFFER_SIZE = 50 * 1024 * 1024 57 private const val TRACE_INTERVAL_US = 1000 58 59 /** 60 * Executes the given function [block] with method tracing to SD card and returns the result of 61 * the function execution. 62 * Tracing is performed with [Debug.startMethodTracingSampling] which uses sampling with 63 * [TRACE_INTERVAL_US] microseconds interval. 64 * Trace file will be stored in [TRACES_FOLDER] and can be pulled via `adb pull` command. 65 * See Logcat output for an exact command to retrieve trace file 66 */ runWithMethodTracingnull67 fun <T> runWithMethodTracing(block: () -> T): T { 68 java.io.File(TRACES_FOLDER).mkdirs() 69 val fileName = SimpleDateFormat(TRACE_NAME_PATTERN, Locale.US).format(Date()) 70 Debug.startMethodTracingSampling( 71 "$TRACES_FOLDER$fileName", 72 BUFFER_SIZE, 73 TRACE_INTERVAL_US 74 ) 75 val result = block() 76 Debug.stopMethodTracing() 77 SharkLog.d { "Method tracing complete! Run the following command to retrieve the trace:" } 78 SharkLog.d { "adb pull $TRACES_FOLDER$fileName ~/Downloads/ " } 79 return result 80 } 81 sleepUntilnull82 private inline fun sleepUntil(condition: () -> Boolean) { 83 while (true) { 84 if (condition()) return else Thread.sleep(SLEEP_TIME_MILLIS) 85 } 86 } 87 samplingThreadExistsnull88 private fun samplingThreadExists() = findThread(SAMPLING_THREAD_NAME) != null 89 90 /** 91 * Utility to get thread by its name; in case of multiple matches first one will be returned. 92 */ 93 private fun findThread(threadName: String): Thread? { 94 // Based on https://stackoverflow.com/a/1323480 95 var rootGroup = Thread.currentThread().threadGroup 96 while (rootGroup.parent != null) rootGroup = rootGroup.parent 97 98 var threads = arrayOfNulls<Thread>(rootGroup.activeCount()) 99 while (rootGroup.enumerate(threads, true) == threads.size) { 100 threads = arrayOfNulls(threads.size * 2) 101 } 102 return threads.firstOrNull { it?.name == threadName } 103 } 104 }