Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 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.os.Build |
| 20 | import android.os.Debug |
| 21 | import android.util.Log |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 22 | import androidx.annotation.RequiresApi |
Chris Craik | 252671f | 2021-12-14 12:27:23 -0800 | [diff] [blame] | 23 | import androidx.annotation.RestrictTo |
Chris Craik | 0ded70b | 2021-11-09 09:08:38 -0800 | [diff] [blame] | 24 | import androidx.benchmark.BenchmarkState.Companion.TAG |
Chris Craik | f7a6c19 | 2022-01-24 16:05:48 -0800 | [diff] [blame] | 25 | import androidx.benchmark.Outputs.dateToFileName |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 26 | import androidx.benchmark.simpleperf.ProfileSession |
| 27 | import androidx.benchmark.simpleperf.RecordOptions |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 28 | |
| 29 | /** |
| 30 | * Profiler abstraction used for the timing stage. |
| 31 | * |
| 32 | * Controlled externally by `androidx.benchmark.profiling.mode` |
| 33 | * Subclasses are objects, as these generally refer to device or process global state. For |
| 34 | * example, things like whether the simpleperf process is running, or whether the runtime is |
| 35 | * capturing method trace. |
| 36 | * |
| 37 | * Note: flags on this class would be simpler if we either had a 'Default'/'Noop' profiler, or a |
| 38 | * wrapper extension function (e.g. `fun Profiler? .requiresSingleMeasurementIteration`). We |
| 39 | * avoid these however, in order to avoid the runtime visiting a new class in the hot path, when |
| 40 | * switching from warmup -> timing phase, when [start] would be called. |
| 41 | */ |
| 42 | internal sealed class Profiler { |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 43 | class ResultFile( |
| 44 | val label: String, |
| 45 | val outputRelativePath: String |
| 46 | ) |
| 47 | |
| 48 | abstract fun start(traceUniqueName: String): ResultFile? |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 49 | abstract fun stop() |
| 50 | |
| 51 | /** |
| 52 | * Measure exactly one loop (one repeat, one iteration). |
| 53 | * |
| 54 | * Generally only set for tracing profilers. |
| 55 | */ |
| 56 | open val requiresSingleMeasurementIteration = false |
| 57 | |
| 58 | /** |
| 59 | * Generally only set for sampling profilers. |
| 60 | */ |
| 61 | open val requiresExtraRuntime = false |
| 62 | |
| 63 | /** |
| 64 | * Currently, debuggable is required to support studio-connected profiling. |
| 65 | * |
| 66 | * Remove this once stable Studio supports profileable. |
| 67 | */ |
| 68 | open val requiresDebuggable = false |
| 69 | |
| 70 | /** |
| 71 | * Connected modes don't need dir, since library isn't doing the capture. |
| 72 | */ |
| 73 | open val requiresLibraryOutputDir = true |
| 74 | |
| 75 | companion object { |
| 76 | const val CONNECTED_PROFILING_SLEEP_MS = 20_000L |
| 77 | |
| 78 | fun getByName(name: String): Profiler? = mapOf( |
Jeff Gaston | 0ecb849 | 2020-09-18 14:06:15 -0400 | [diff] [blame] | 79 | "MethodTracing" to MethodTracing, |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 80 | |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 81 | "StackSampling" to if (Build.VERSION.SDK_INT >= 29) { |
| 82 | StackSamplingSimpleperf // only supported on 29+ without root/debug/sideload |
| 83 | } else { |
| 84 | StackSamplingLegacy |
| 85 | }, |
| 86 | |
Jeff Gaston | 0ecb849 | 2020-09-18 14:06:15 -0400 | [diff] [blame] | 87 | "ConnectedAllocation" to ConnectedAllocation, |
| 88 | "ConnectedSampling" to ConnectedSampling, |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 89 | |
Jeff Gaston | 0ecb849 | 2020-09-18 14:06:15 -0400 | [diff] [blame] | 90 | // Below are compat codepaths for old names. Remove before 1.1 stable. |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 91 | |
| 92 | "MethodSampling" to StackSamplingLegacy, |
| 93 | "MethodSamplingSimpleperf" to StackSamplingSimpleperf, |
Jeff Gaston | 0ecb849 | 2020-09-18 14:06:15 -0400 | [diff] [blame] | 94 | "Method" to MethodTracing, |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 95 | "Sampled" to StackSamplingLegacy, |
Jeff Gaston | 0ecb849 | 2020-09-18 14:06:15 -0400 | [diff] [blame] | 96 | "ConnectedSampled" to ConnectedSampling |
| 97 | ) |
Jim Sproch | e238bad | 2021-03-23 16:47:15 -0700 | [diff] [blame] | 98 | .mapKeys { it.key.lowercase() }[name.lowercase()] |
Chris Craik | f7a6c19 | 2022-01-24 16:05:48 -0800 | [diff] [blame] | 99 | |
| 100 | fun traceName(traceUniqueName: String, traceTypeLabel: String): String { |
| 101 | return "$traceUniqueName-$traceTypeLabel-${dateToFileName()}.trace" |
| 102 | } |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 103 | } |
| 104 | } |
| 105 | |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 106 | internal fun startRuntimeMethodTracing( |
| 107 | traceFileName: String, |
| 108 | sampled: Boolean |
| 109 | ): Profiler.ResultFile { |
Rahul Ravikumar | 56428c0 | 2021-03-04 12:12:03 -0800 | [diff] [blame] | 110 | val path = Outputs.testOutputFile(traceFileName).absolutePath |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 111 | |
Chris Craik | ec8b0e2 | 2021-11-19 14:52:10 -0800 | [diff] [blame] | 112 | Log.d(TAG, "Profiling output file: $path") |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 113 | InstrumentationResults.reportAdditionalFileToCopy("profiling_trace", path) |
| 114 | |
| 115 | val bufferSize = 16 * 1024 * 1024 |
| 116 | if (sampled && |
| 117 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP |
| 118 | ) { |
Chris Craik | 54db19e | 2021-07-13 12:49:06 -0700 | [diff] [blame] | 119 | startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency) |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 120 | } else { |
| 121 | Debug.startMethodTracing(path, bufferSize, 0) |
| 122 | } |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 123 | |
| 124 | return Profiler.ResultFile( |
| 125 | outputRelativePath = traceFileName, |
| 126 | label = if (sampled) "Stack Sampling (legacy) Trace" else "Method Trace" |
| 127 | ) |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 128 | } |
| 129 | |
| 130 | internal fun stopRuntimeMethodTracing() { |
| 131 | Debug.stopMethodTracing() |
| 132 | } |
| 133 | |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 134 | internal object StackSamplingLegacy : Profiler() { |
Chris Craik | f7a6c19 | 2022-01-24 16:05:48 -0800 | [diff] [blame] | 135 | @get:RestrictTo(RestrictTo.Scope.TESTS) |
Chris Craik | 252671f | 2021-12-14 12:27:23 -0800 | [diff] [blame] | 136 | var isRunning = false |
| 137 | |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 138 | override fun start(traceUniqueName: String): ResultFile { |
| 139 | isRunning = true |
| 140 | return startRuntimeMethodTracing( |
Chris Craik | f7a6c19 | 2022-01-24 16:05:48 -0800 | [diff] [blame] | 141 | traceFileName = traceName(traceUniqueName, "stackSamplingLegacy"), |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 142 | sampled = true |
| 143 | ) |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 144 | } |
| 145 | |
| 146 | override fun stop() { |
| 147 | stopRuntimeMethodTracing() |
Chris Craik | 252671f | 2021-12-14 12:27:23 -0800 | [diff] [blame] | 148 | isRunning = false |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 149 | } |
| 150 | |
| 151 | override val requiresExtraRuntime: Boolean = true |
| 152 | } |
| 153 | |
| 154 | internal object MethodTracing : Profiler() { |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 155 | override fun start(traceUniqueName: String): ResultFile { |
| 156 | return startRuntimeMethodTracing( |
Chris Craik | f7a6c19 | 2022-01-24 16:05:48 -0800 | [diff] [blame] | 157 | traceFileName = traceName(traceUniqueName, "methodTracing"), |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 158 | sampled = false |
| 159 | ) |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 160 | } |
| 161 | |
| 162 | override fun stop() { |
| 163 | stopRuntimeMethodTracing() |
| 164 | } |
| 165 | |
| 166 | override val requiresSingleMeasurementIteration: Boolean = true |
| 167 | } |
| 168 | |
| 169 | internal object ConnectedAllocation : Profiler() { |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 170 | override fun start(traceUniqueName: String): ResultFile? { |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 171 | Thread.sleep(CONNECTED_PROFILING_SLEEP_MS) |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 172 | return null |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 173 | } |
| 174 | |
| 175 | override fun stop() { |
| 176 | Thread.sleep(CONNECTED_PROFILING_SLEEP_MS) |
| 177 | } |
| 178 | |
| 179 | override val requiresSingleMeasurementIteration: Boolean = true |
| 180 | override val requiresDebuggable: Boolean = true |
| 181 | override val requiresLibraryOutputDir: Boolean = false |
| 182 | } |
| 183 | |
| 184 | internal object ConnectedSampling : Profiler() { |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 185 | override fun start(traceUniqueName: String): ResultFile? { |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 186 | Thread.sleep(CONNECTED_PROFILING_SLEEP_MS) |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 187 | return null |
Chris Craik | dfae368 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | override fun stop() { |
| 191 | Thread.sleep(CONNECTED_PROFILING_SLEEP_MS) |
| 192 | } |
| 193 | |
| 194 | override val requiresDebuggable: Boolean = true |
| 195 | override val requiresLibraryOutputDir: Boolean = false |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 196 | } |
| 197 | |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 198 | /** |
| 199 | * Simpleperf profiler. |
| 200 | * |
| 201 | * API 29+ currently, since it relies on the platform system image simpleperf. |
| 202 | * |
| 203 | * Could potentially lower, but that would require root or debuggable. |
| 204 | */ |
| 205 | internal object StackSamplingSimpleperf : Profiler() { |
| 206 | @RequiresApi(29) |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 207 | private var session: ProfileSession? = null |
| 208 | |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 209 | /** "security.perf_harden" must be set to "0" during simpleperf capture */ |
| 210 | @RequiresApi(29) |
| 211 | private val securityPerfHarden = PropOverride("security.perf_harden", "0") |
| 212 | |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 213 | var outputRelativePath: String? = null |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 214 | |
| 215 | @RequiresApi(29) |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 216 | override fun start(traceUniqueName: String): ResultFile? { |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 217 | session?.stopRecording() // stop previous |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 218 | |
| 219 | // for security perf harden, enable temporarily |
| 220 | securityPerfHarden.forceValue() |
| 221 | |
| 222 | // for all other properties, simply set the values, as these don't have defaults |
| 223 | Shell.executeCommand("setprop debug.perf_event_max_sample_rate 10000") |
| 224 | Shell.executeCommand("setprop debug.perf_cpu_time_max_percent 25") |
| 225 | Shell.executeCommand("setprop debug.perf_event_mlock_kb 32800") |
| 226 | |
Chris Craik | f7a6c19 | 2022-01-24 16:05:48 -0800 | [diff] [blame] | 227 | outputRelativePath = traceName(traceUniqueName, "stackSampling") |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 228 | session = ProfileSession().also { |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 229 | // prepare simpleperf must be done as shell user, so do this here with other shell setup |
| 230 | // NOTE: this is sticky across reboots, so missing this will cause tests or profiling to |
| 231 | // fail, but only on devices that have not run this command since flashing (e.g. in CI) |
| 232 | Shell.executeCommand(it.findSimpleperf() + " api-prepare") |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 233 | it.startRecording( |
| 234 | RecordOptions() |
| 235 | .setSampleFrequency(Arguments.profilerSampleFrequency) |
| 236 | .recordDwarfCallGraph() // enable Java/Kotlin callstacks |
| 237 | .traceOffCpu() // track time sleeping |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 238 | .setOutputFilename("simpleperf.data") |
| 239 | .apply { |
Chris Craik | 0ded70b | 2021-11-09 09:08:38 -0800 | [diff] [blame] | 240 | // some emulators don't support cpu-cycles, the default event, so instead we |
| 241 | // use cpu-clock, which is a software perf event using kernel hrtimer to |
| 242 | // generate interrupts |
| 243 | val hwEventsOutput = Shell.executeCommand("simpleperf list hw").trim() |
| 244 | check(hwEventsOutput.startsWith("List of hardware events:")) |
| 245 | val events = hwEventsOutput |
| 246 | .split("\n") |
| 247 | .drop(1) |
| 248 | .map { line -> line.trim() } |
| 249 | if (!events.any { hwEvent -> hwEvent.trim() == "cpu-cycles" }) { |
| 250 | Log.d(TAG, "cpu-cycles not found - using cpu-clock (events = $events)") |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 251 | setEvent("cpu-clock") |
| 252 | } |
| 253 | } |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 254 | ) |
| 255 | } |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 256 | return ResultFile( |
| 257 | label = "Stack Sampling Trace", |
| 258 | outputRelativePath = outputRelativePath!! |
| 259 | ) |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 260 | } |
| 261 | |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 262 | @RequiresApi(29) |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 263 | override fun stop() { |
| 264 | session!!.stopRecording() |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 265 | Outputs.writeFile( |
Chris Craik | edaec69 | 2022-01-13 15:31:16 -0800 | [diff] [blame] | 266 | fileName = outputRelativePath!!, |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 267 | reportKey = "simpleperf_trace" |
| 268 | ) { |
| 269 | session!!.convertSimpleperfOutputToProto("simpleperf.data", it.absolutePath) |
| 270 | } |
| 271 | |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 272 | session = null |
Chris Craik | c28da96 | 2021-08-05 14:55:18 -0700 | [diff] [blame] | 273 | securityPerfHarden.resetIfOverridden() |
Chris Craik | 42d4248 | 2020-06-17 11:32:08 -0700 | [diff] [blame] | 274 | } |
| 275 | |
| 276 | override val requiresLibraryOutputDir: Boolean = false |
Alexander Dorokhine | ca788f8 | 2021-06-08 12:33:49 -0700 | [diff] [blame] | 277 | } |