1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.benchmark
18 
19 import android.annotation.SuppressLint
20 import android.os.Build
21 import android.os.Debug
22 import android.os.Looper
23 import android.util.Log
24 import androidx.annotation.RequiresApi
25 import androidx.annotation.RestrictTo
26 import androidx.annotation.VisibleForTesting
27 import androidx.benchmark.BenchmarkState.Companion.METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR
28 import androidx.benchmark.BenchmarkState.Companion.METHOD_TRACING_MAX_DURATION_NS
29 import androidx.benchmark.BenchmarkState.Companion.TAG
30 import androidx.benchmark.Outputs.dateToFileName
31 import androidx.benchmark.json.BenchmarkData.TestResult.ProfilerOutput
32 import androidx.benchmark.perfetto.StackSamplingConfig
33 import androidx.benchmark.simpleperf.ProfileSession
34 import androidx.benchmark.simpleperf.RecordOptions
35 import androidx.benchmark.vmtrace.ArtTrace
36 import java.io.File
37 import java.io.FileOutputStream
38 
39 /**
40  * Profiler abstraction used for the timing stage.
41  *
42  * Controlled externally by `androidx.benchmark.profiling.mode` Subclasses are objects, as these
43  * generally refer to device or process global state. For example, things like whether the
44  * simpleperf process is running, or whether the runtime is capturing method trace.
45  *
46  * Note: flags on this class would be simpler if we either had a 'Default'/'Noop' profiler, or a
47  * wrapper extension function (e.g. `fun Profiler? .requiresSingleMeasurementIteration`). We avoid
48  * these however, in order to avoid the runtime visiting a new class in the hot path, when switching
49  * from warmup -> timing phase, when [start] would be called.
50  */
51 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
52 sealed class Profiler() {
53     class ResultFile
54     private constructor(
55         val label: String,
56         val type: ProfilerOutput.Type,
57         val outputRelativePath: String,
58         val source: Profiler?,
59         val convertBeforeSync: (() -> Unit)? = null
60     ) {
61 
embedInPerfettoTracenull62         fun embedInPerfettoTrace(perfettoTracePath: String) {
63             source?.embedInPerfettoTrace(
64                 File(Outputs.outputDirectory, outputRelativePath),
65                 File(perfettoTracePath)
66             )
67         }
68 
69         companion object {
ofPerfettoTracenull70             fun ofPerfettoTrace(label: String, absolutePath: String) =
71                 ResultFile(
72                     label = label,
73                     outputRelativePath = Outputs.relativePathFor(absolutePath),
74                     type = ProfilerOutput.Type.PerfettoTrace,
75                     source = null
76                 )
77 
78             fun ofMethodTrace(label: String, absolutePath: String) =
79                 ResultFile(
80                     label = label,
81                     outputRelativePath = Outputs.relativePathFor(absolutePath),
82                     type = ProfilerOutput.Type.MethodTrace,
83                     source = null
84                 )
85 
86             fun of(
87                 label: String,
88                 type: ProfilerOutput.Type,
89                 outputRelativePath: String,
90                 source: Profiler,
91                 convertBeforeSync: (() -> Unit)? = null
92             ) =
93                 ResultFile(
94                     label = label,
95                     outputRelativePath = outputRelativePath,
96                     type = type,
97                     source = source,
98                     convertBeforeSync = convertBeforeSync
99                 )
100         }
101     }
102 
103     abstract fun start(traceUniqueName: String): ResultFile?
104 
105     /** Start profiling only if expected trace duration is unlikely to trigger an ANR */
106     fun startIfNotRiskingAnrDeadline(
107         traceUniqueName: String,
108         estimatedDurationNs: Long
109     ): ResultFile? {
110         val estimatedMethodTraceDurNs =
111             estimatedDurationNs * METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR
112         return if (
113             this == MethodTracing &&
114                 Looper.myLooper() == Looper.getMainLooper() &&
115                 estimatedMethodTraceDurNs > METHOD_TRACING_MAX_DURATION_NS &&
116                 Arguments.profilerSkipWhenDurationRisksAnr
117         ) {
118             val expectedDurSec = estimatedMethodTraceDurNs / 1_000_000_000.0
119             InstrumentationResults.scheduleIdeWarningOnNextReport(
120                 """
121                         Skipping method trace of estimated duration $expectedDurSec sec to avoid ANR
122 
123                         To disable this behavior, set instrumentation arg:
124                             androidx.benchmark.profiling.skipWhenDurationRisksAnr = false
125                     """
126                     .trimIndent()
127             )
128             null
129         } else {
130             start(traceUniqueName)
131         }
132     }
133 
stopnull134     abstract fun stop()
135 
136     internal open fun config(packageNames: List<String>): StackSamplingConfig? = null
137 
138     open fun embedInPerfettoTrace(profilerTrace: File, perfettoTrace: File) {}
139 
140     /**
141      * Measure exactly one loop (one repeat, one iteration).
142      *
143      * Generally only set for tracing profilers.
144      */
145     open val requiresSingleMeasurementIteration = false
146 
147     /** Generally only set for sampling profilers. */
148     open val requiresExtraRuntime = false
149 
150     /**
151      * Currently, debuggable is required to support studio-connected profiling.
152      *
153      * Remove this once stable Studio supports profileable.
154      */
155     open val requiresDebuggable = false
156 
157     /** Connected modes don't need dir, since library isn't doing the capture. */
158     open val requiresLibraryOutputDir = true
159 
160     companion object {
161         const val CONNECTED_PROFILING_SLEEP_MS = 20_000L
162 
getByNamenull163         fun getByName(name: String): Profiler? =
164             mapOf(
165                     "MethodTracing" to MethodTracing,
166                     "StackSampling" to
167                         if (Build.VERSION.SDK_INT >= 29) {
168                             StackSamplingSimpleperf // only supported on 29+ without
169                             // root/debug/sideload
170                         } else {
171                             StackSamplingLegacy
172                         },
173                     "ConnectedAllocation" to ConnectedAllocation,
174                     "ConnectedSampling" to ConnectedSampling,
175 
176                     // Below are compat codepaths for old names. Remove before 1.1 stable.
177 
178                     "MethodSampling" to StackSamplingLegacy,
179                     "MethodSamplingSimpleperf" to StackSamplingSimpleperf,
180                     "Method" to MethodTracing,
181                     "Sampled" to StackSamplingLegacy,
182                     "ConnectedSampled" to ConnectedSampling
183                 )
<lambda>null184                 .mapKeys { it.key.lowercase() }[name.lowercase()]
185 
traceNamenull186         fun traceName(traceUniqueName: String, traceTypeLabel: String): String {
187             return Outputs.sanitizeFilename(
188                 "$traceUniqueName-$traceTypeLabel-${dateToFileName()}.trace"
189             )
190         }
191     }
192 }
193 
startRuntimeMethodTracingnull194 internal fun startRuntimeMethodTracing(
195     traceFileName: String,
196     sampled: Boolean,
197     profiler: Profiler,
198 ): Profiler.ResultFile {
199     val path = Outputs.testOutputFile(traceFileName).absolutePath
200 
201     Log.d(TAG, "Profiling output file: $path")
202     InstrumentationResults.reportAdditionalFileToCopy("profiling_trace", path)
203 
204     val bufferSize = 16 * 1024 * 1024
205 
206     // Note: The last thing this method does is start profiling,
207     // since we want to capture as little benchmark infra as possible
208     return if (sampled) {
209         val intervalUs = (1_000_000.0 / Arguments.profilerSampleFrequencyHz).toInt()
210         Profiler.ResultFile.of(
211                 outputRelativePath = traceFileName,
212                 label = "Stack Sampling (legacy) Trace",
213                 type = ProfilerOutput.Type.StackSamplingTrace,
214                 source = profiler
215             )
216             .also { Debug.startMethodTracingSampling(path, bufferSize, intervalUs) }
217     } else {
218         Profiler.ResultFile.of(
219                 outputRelativePath = traceFileName,
220                 label = "Method Trace",
221                 type = ProfilerOutput.Type.MethodTrace,
222                 source = profiler
223             )
224             .also {
225                 // NOTE: 0x10 flag enables low-overhead wall clock timing when ART module version
226                 // supports
227                 // it. Note that this doesn't affect trace parsing, since this doesn't affect wall
228                 // clock,
229                 // it only removes the expensive thread time clock which our parser doesn't use.
230                 // TODO: switch to platform-defined constant once available (b/329499422)
231                 Debug.startMethodTracing(path, bufferSize, 0x10)
232             }
233     }
234 }
235 
stopRuntimeMethodTracingnull236 internal fun stopRuntimeMethodTracing() {
237     Debug.stopMethodTracing()
238 }
239 
240 internal object StackSamplingLegacy : Profiler() {
241     @get:VisibleForTesting var isRunning = false
242 
startnull243     override fun start(traceUniqueName: String): ResultFile {
244         isRunning = true
245         return startRuntimeMethodTracing(
246             traceFileName = traceName(traceUniqueName, "stackSamplingLegacy"),
247             sampled = true,
248             profiler = this
249         )
250     }
251 
stopnull252     override fun stop() {
253         stopRuntimeMethodTracing()
254         isRunning = false
255     }
256 
257     override val requiresExtraRuntime: Boolean = true
258 }
259 
260 internal object MethodTracing : Profiler() {
startnull261     override fun start(traceUniqueName: String): ResultFile {
262         hasBeenUsed = true
263         return startRuntimeMethodTracing(
264             traceFileName = traceName(traceUniqueName, "methodTracing"),
265             sampled = false,
266             profiler = this
267         )
268     }
269 
stopnull270     override fun stop() {
271         stopRuntimeMethodTracing()
272     }
273 
274     override val requiresSingleMeasurementIteration: Boolean = true
275 
embedInPerfettoTracenull276     override fun embedInPerfettoTrace(profilerTrace: File, perfettoTrace: File) {
277         ArtTrace(profilerTrace)
278             .writeAsPerfettoTrace(FileOutputStream(perfettoTrace, /* append= */ true))
279     }
280 
281     var hasBeenUsed: Boolean = false
282         private set
283 }
284 
285 @SuppressLint("BanThreadSleep") // needed for connected profiling
286 internal object ConnectedAllocation : Profiler() {
startnull287     override fun start(traceUniqueName: String): ResultFile? {
288         Thread.sleep(CONNECTED_PROFILING_SLEEP_MS)
289         return null
290     }
291 
stopnull292     override fun stop() {
293         Thread.sleep(CONNECTED_PROFILING_SLEEP_MS)
294     }
295 
296     override val requiresSingleMeasurementIteration: Boolean = true
297     override val requiresDebuggable: Boolean = true
298     override val requiresLibraryOutputDir: Boolean = false
299 }
300 
301 @SuppressLint("BanThreadSleep") // needed for connected profiling
302 internal object ConnectedSampling : Profiler() {
startnull303     override fun start(traceUniqueName: String): ResultFile? {
304         Thread.sleep(CONNECTED_PROFILING_SLEEP_MS)
305         return null
306     }
307 
stopnull308     override fun stop() {
309         Thread.sleep(CONNECTED_PROFILING_SLEEP_MS)
310     }
311 
312     override val requiresDebuggable: Boolean = true
313     override val requiresLibraryOutputDir: Boolean = false
314 }
315 
316 /**
317  * Simpleperf profiler.
318  *
319  * API 29+ currently, since it relies on the platform system image simpleperf.
320  *
321  * Could potentially lower, but that would require root or debuggable.
322  */
323 internal object StackSamplingSimpleperf : Profiler() {
324 
325     @RequiresApi(29) private var session: ProfileSession? = null
326 
327     /** "security.perf_harden" must be set to "0" during simpleperf capture */
328     @RequiresApi(29) private val securityPerfHarden = PropOverride("security.perf_harden", "0")
329 
330     private var outputRelativePath: String? = null
331 
332     @RequiresApi(29)
startnull333     override fun start(traceUniqueName: String): ResultFile {
334         session?.stopRecording() // stop previous
335 
336         // for security perf harden, enable temporarily
337         securityPerfHarden.forceValue()
338 
339         // for all other properties, simply set the values, as these don't have defaults
340         Shell.executeScriptSilent("setprop debug.perf_event_max_sample_rate 10000")
341         Shell.executeScriptSilent("setprop debug.perf_cpu_time_max_percent 25")
342         Shell.executeScriptSilent("setprop debug.perf_event_mlock_kb 32800")
343 
344         outputRelativePath = traceName(traceUniqueName, "stackSampling")
345         session =
346             ProfileSession().also {
347                 // prepare simpleperf must be done as shell user, so do this here with other shell
348                 // setup
349                 // NOTE: this is sticky across reboots, so missing this will cause tests or
350                 // profiling to
351                 // fail, but only on devices that have not run this command since flashing (e.g. in
352                 // CI)
353                 Shell.executeScriptSilent(it.findSimpleperf() + " api-prepare")
354                 it.startRecording(
355                     RecordOptions()
356                         .setSampleFrequency(Arguments.profilerSampleFrequencyHz)
357                         .recordDwarfCallGraph() // enable Java/Kotlin callstacks
358                         .setEvent("cpu-clock") // Required on API 33 to enable traceOffCpu
359                         .traceOffCpu() // track time sleeping
360                         .setSampleCurrentThread() // sample stacks from this thread only
361                         .setOutputFilename("simpleperf.data")
362                 )
363             }
364         return ResultFile.of(
365             label = "Stack Sampling Trace",
366             outputRelativePath = outputRelativePath!!,
367             type = ProfilerOutput.Type.StackSamplingTrace,
368             source = this,
369             convertBeforeSync = this::convertBeforeSync
370         )
371     }
372 
373     @RequiresApi(29)
stopnull374     override fun stop() {
375         session!!.stopRecording()
376         securityPerfHarden.resetIfOverridden()
377     }
378 
379     @RequiresApi(29)
convertBeforeSyncnull380     fun convertBeforeSync() {
381         Outputs.writeFile(fileName = outputRelativePath!!) {
382             session!!.convertSimpleperfOutputToProto("simpleperf.data", it.absolutePath)
383             session = null
384         }
385     }
386 
confignull387     override fun config(packageNames: List<String>) =
388         StackSamplingConfig(
389             packageNames = packageNames,
390             frequency = Arguments.profilerSampleFrequencyHz.toLong(),
391             duration = Arguments.profilerSampleDurationSeconds,
392         )
393 
394     override val requiresLibraryOutputDir: Boolean = false
395 
396     override val requiresExtraRuntime: Boolean = true
397 }
398