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