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 }