1 /*
<lambda>null2 * Copyright 2021 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.macro
18
19 import android.os.Build
20 import android.util.Log
21 import androidx.annotation.IntRange
22 import androidx.annotation.RequiresApi
23 import androidx.annotation.RestrictTo
24 import androidx.benchmark.Arguments
25 import androidx.benchmark.DeviceInfo
26 import androidx.benchmark.Shell
27 import androidx.benchmark.inMemoryTrace
28 import androidx.profileinstaller.ProfileInstallReceiver
29 import org.junit.AssumptionViolatedException
30
31 /**
32 * Type of compilation to use for a Macrobenchmark.
33 *
34 * This compilation mode controls pre-compilation that occurs before running the setup / measure
35 * blocks of the benchmark.
36 *
37 * On Android N+ (API 24+), there are different levels of compilation supported:
38 * * [Partial] - the default configuration of [Partial] will partially pre-compile your application,
39 * if a Baseline Profile is included in your app. This represents the most realistic fresh-install
40 * experience on an end-user's device. You can additionally or instead use
41 * [Partial.warmupIterations] to use Profile Guided Optimization, using the benchmark content to
42 * guide pre-compilation. This can mimic an application's performance after background dexopt has
43 * partially compiled the app during device idle time, after the app has been used (generally
44 * after a day or more of usage after update/install).
45 * * [Full] - the app is fully pre-compiled. This is generally not representative of real user
46 * experience, as apps are not fully pre-compiled on user devices more recent than Android N (API
47 * 24). `Full` can be used to show unrealistic but potentially more stable performance by removing
48 * the noise/inconsistency from just-in-time compilation within benchmark runs. Note that `Full`
49 * compilation will often be slower than [Partial] compilation, as the increased code size creates
50 * more cost for disk loading during startup, and increases pressure in the instruction cache.
51 * * [None] - the app isn't pre-compiled at all, bypassing the default compilation that should
52 * generally be done at install time, e.g. by the Play Store. This will illustrate worst case
53 * performance, and will show you performance of your app if you do not enable baseline profiles,
54 * useful for judging the performance impact of the baseline profiles included in your
55 * application.
56 * * [Ignore] - the state of compilation will be ignored. The intended use-case is for a developer
57 * to customize the compilation state for an app; and then tell Macrobenchmark to leave it
58 * unchanged.
59 *
60 * On Android M (API 23), only [Full] is supported, as all apps are always fully compiled.
61 *
62 * To understand more how these modes work, you can see comments for each class, and also see the
63 * [Android Runtime compilation modes](https://source.android.com/devices/tech/dalvik/configure#compilation_options)
64 * (which are passed by benchmark into
65 * [`cmd compile`](https://source.android.com/devices/tech/dalvik/jit-compiler#force-compilation-of-a-specific-package)
66 * to compile the target app).
67 */
68 sealed class CompilationMode {
69 internal fun resetAndCompile(
70 scope: MacrobenchmarkScope,
71 allowCompilationSkipping: Boolean = true,
72 warmupBlock: () -> Unit,
73 ) {
74 val packageName = scope.packageName
75 if (Build.VERSION.SDK_INT >= 24) {
76 if (Arguments.enableCompilation || !allowCompilationSkipping) {
77 Log.d(TAG, "Clearing ART profiles for $packageName")
78 // The compilation mode chooses whether a reset is required or not.
79 // Currently the only compilation mode that does not perform a reset is
80 // CompilationMode.Ignore.
81 if (shouldReset()) {
82 // Package reset enabled
83 Log.d(TAG, "Resetting profiles for $packageName")
84 // It's not possible to reset the compilation profile on `user` builds.
85 // The flag `enablePackageReset` can be set to `true` on `userdebug` builds in
86 // order to speed-up the profile reset. When set to false, reset is performed
87 // uninstalling and reinstalling the app.
88 if (Build.VERSION.SDK_INT >= 34) {
89 // Starting API 34, --reset restores the state of the compiled code based
90 // on prior install state. This means, e.g. if AGP version 8.3+ installs a
91 // DM alongside the APK, reset != clear.
92 // Use --verify to replace the contents of the odex file with that of an
93 // empty file.
94 cmdPackageCompile(packageName, "verify")
95 // This does not clear the state of the `cur` and `ref` profiles.
96 // To do that we also need to call `pm art clear-app-profiles <package>`.
97 // pm art clear-app-profiles returns a "Profiles cleared"
98 // to stdout upon success. Otherwise it includes an Error: <error reason>.
99 val output =
100 Shell.executeScriptCaptureStdout(
101 "pm art clear-app-profiles $packageName"
102 )
103
104 check(output.trim() == "Profiles cleared") {
105 compileResetErrorString(packageName, output, DeviceInfo.isEmulator)
106 }
107 } else if (Shell.isSessionRooted()) {
108 cmdPackageCompileReset(packageName)
109 } else {
110 // User builds pre-U. Kick off a full uninstall-reinstall
111 Log.d(TAG, "Reinstalling $packageName")
112 reinstallPackage(packageName)
113 }
114 }
115 // Write skip file to stop profile installer from interfering with the benchmark
116 writeProfileInstallerSkipFile(scope)
117 compileImpl(scope, warmupBlock)
118 } else {
119 Log.d(TAG, "Compilation is disabled, skipping compilation of $packageName")
120 }
121 }
122 }
123
124 /**
125 * A more expensive alternative to `compile --reset` which doesn't preserve app data, but does
126 * work on older APIs without root.
127 */
128 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
129 fun reinstallPackage(packageName: String) {
130 inMemoryTrace("reinstallPackage") {
131 val copiedApkPaths = copiedApkPaths(packageName)
132 try {
133 // Uninstall package
134 // This is what effectively clears the ART profiles
135 uninstallPackage(packageName)
136 // Install the APK from /data/local/tmp
137 installPackageFromPaths(packageName = packageName, copiedApkPaths = copiedApkPaths)
138 } finally {
139 // Cleanup the temporary APK
140 Log.d(TAG, "Deleting $copiedApkPaths")
141 Shell.rm(copiedApkPaths)
142 }
143 }
144 }
145
146 /**
147 * Copies the APKs obtained from the current install location, into `/data/local/tmp` and
148 * returns a `<space>` delimited list of paths that can be used to reinstall the app package
149 * after uninstall.
150 */
151 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
152 fun copiedApkPaths(packageName: String): String {
153 // Copy APKs to /data/local/temp
154 val apkPaths = Shell.pmPath(packageName)
155
156 val tempApkPaths: List<String> =
157 apkPaths.mapIndexed { index, apkPath ->
158 val tempApkPath =
159 "/data/local/tmp/$packageName-$index-${System.currentTimeMillis()}.apk"
160 Log.d(TAG, "Copying APK $apkPath to $tempApkPath")
161 Shell.cp(from = apkPath, to = tempApkPath)
162 tempApkPath
163 }
164 return tempApkPaths.joinToString(" ")
165 }
166
167 /** Uninstalls an app package by using `pm uninstall` under the hood. */
168 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
169 fun uninstallPackage(packageName: String) {
170 Log.d(TAG, "Uninstalling $packageName")
171 val output = Shell.executeScriptCaptureStdout("pm uninstall $packageName")
172 check(output.trim() == "Success") { "Unable to uninstall $packageName ($output)" }
173 }
174
175 /**
176 * Installs the app using a set of APKs that were previously copied and staged into
177 * `/data/local/tmp` from a pre-existing install session.
178 */
179 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
180 fun installPackageFromPaths(packageName: String, copiedApkPaths: String) {
181 Log.d(TAG, "Installing $packageName")
182 val builder = StringBuilder("pm install")
183 // Provide a `-t` argument to `pm install` to ensure test packages are
184 // correctly installed. (b/231294733)
185 builder.append(" -t")
186 if (Build.VERSION.SDK_INT >= 30) {
187 // Use --skip-verification to disable Play protect.
188 // This option was introduced in Android R (30)
189 // b/308100444 has additional context.
190 builder.append(" --skip-verification")
191 }
192 builder.append(" $copiedApkPaths")
193 val output = Shell.executeScriptCaptureStdout(builder.toString())
194
195 check(output.trim() == "Success" || output.contains("PERFORMED")) {
196 "Unable to install $packageName (out=$output)"
197 }
198 }
199
200 /**
201 * Writes a skip file via a [ProfileInstallReceiver] broadcast, so profile installation does not
202 * interfere with benchmarks.
203 */
204 private fun writeProfileInstallerSkipFile(scope: MacrobenchmarkScope) {
205 val packageName = scope.packageName
206 val result = ProfileInstallBroadcast.skipFileOperation(packageName, "WRITE_SKIP_FILE")
207 if (result != null) {
208 Log.w(
209 TAG,
210 """
211 $packageName should use the latest version of `androidx.profileinstaller`
212 for stable benchmarks. ($result)"
213 """
214 .trimIndent()
215 )
216 }
217 Log.d(TAG, "Killing process $packageName")
218 scope.killProcess()
219 }
220
221 @RequiresApi(24)
222 internal abstract fun compileImpl(
223 scope: MacrobenchmarkScope,
224 warmupBlock: () -> Unit,
225 )
226
227 @RequiresApi(24) internal abstract fun shouldReset(): Boolean
228
229 internal open fun requiresClearArtRuntimeImage(): Boolean = false
230
231 /**
232 * No pre-compilation - a compilation profile reset is performed and the entire app will be
233 * allowed to Just-In-Time compile as it runs.
234 *
235 * Note that later iterations may perform differently if the app is not killed each iteration
236 * (such as will `StartupMode.COLD`), as app code is jitted.
237 */
238 @Suppress("CanSealedSubClassBeObject")
239 @RequiresApi(24)
240 class None : CompilationMode() {
241
242 override fun toString(): String = "None"
243
244 override fun compileImpl(scope: MacrobenchmarkScope, warmupBlock: () -> Unit) {
245 // nothing to do!
246 }
247
248 override fun shouldReset(): Boolean = true
249
250 /**
251 * To get worst-case `cmd package compile -f -m verify` performance on API 34+, we must
252 * clear the art runtime *EACH TIME* the app is killed.
253 */
254 override fun requiresClearArtRuntimeImage(): Boolean {
255 return DeviceInfo.supportsRuntimeImages
256 }
257 }
258
259 /**
260 * This compilation mode doesn't perform any reset or compilation, leaving the user the choice
261 * to implement these steps.
262 */
263 // Leaving possibility for future configuration
264 @ExperimentalMacrobenchmarkApi
265 @Suppress("CanSealedSubClassBeObject")
266 class Ignore : CompilationMode() {
267 override fun toString(): String = "Ignore"
268
269 override fun compileImpl(scope: MacrobenchmarkScope, warmupBlock: () -> Unit) {
270 // Do nothing.
271 }
272
273 override fun shouldReset(): Boolean = false
274 }
275
276 /**
277 * Partial ahead-of-time app compilation.
278 *
279 * The default parameters for this mimic the default state of an app partially pre-compiled by
280 * the installer - such as via Google Play.
281 *
282 * Either [baselineProfileMode] must be set to non-[BaselineProfileMode.Disable], or
283 * [warmupIterations] must be set to a non-`0` value.
284 *
285 * Note: `[baselineProfileMode] = [BaselineProfileMode.Require]` is only supported for APKs that
286 * have the ProfileInstaller library included, and have been built by AGP 7.0+ to package the
287 * baseline profile in the APK.
288 */
289 @RequiresApi(24)
290 class Partial
291 @JvmOverloads
292 constructor(
293 /**
294 * Controls whether a Baseline Profile should be used to partially pre compile the app.
295 *
296 * Defaults to [BaselineProfileMode.Require]
297 *
298 * @see BaselineProfileMode
299 */
300 val baselineProfileMode: BaselineProfileMode = BaselineProfileMode.Require,
301
302 /**
303 * If greater than 0, your macrobenchmark will run an extra [warmupIterations] times before
304 * compilation, to prepare
305 */
306 @IntRange(from = 0) val warmupIterations: Int = 0
307 ) : CompilationMode() {
308 init {
309 require(warmupIterations >= 0) {
310 "warmupIterations must be non-negative, was $warmupIterations"
311 }
312 require(baselineProfileMode != BaselineProfileMode.Disable || warmupIterations > 0) {
313 "Must set baselineProfileMode != Ignore, or warmup iterations > 0 to define" +
314 " which portion of the app to pre-compile."
315 }
316 }
317
318 override fun toString(): String {
319 return if (
320 baselineProfileMode == BaselineProfileMode.Require && warmupIterations == 0
321 ) {
322 "BaselineProfile"
323 } else if (baselineProfileMode == BaselineProfileMode.Disable && warmupIterations > 0) {
324 "WarmupProfile(iterations=$warmupIterations)"
325 } else {
326 "Partial(baselineProfile=$baselineProfileMode,iterations=$warmupIterations)"
327 }
328 }
329
330 override fun compileImpl(scope: MacrobenchmarkScope, warmupBlock: () -> Unit) {
331 val packageName = scope.packageName
332 if (baselineProfileMode != BaselineProfileMode.Disable) {
333 // Ignores the presence of a skip file.
334 val installErrorString = ProfileInstallBroadcast.installProfile(packageName)
335 if (installErrorString == null) {
336 // baseline profile install success, kill process before compiling
337 Log.d(TAG, "Killing process $packageName")
338 // We don't really need to flush ART profiles here, but its safer to do it.
339 scope.killProcess()
340 cmdPackageCompile(packageName, "speed-profile")
341 } else {
342 if (baselineProfileMode == BaselineProfileMode.Require) {
343 throw RuntimeException(installErrorString)
344 } else {
345 Log.d(TAG, installErrorString)
346 }
347 }
348 }
349 if (warmupIterations > 0) {
350 scope.withKillMode(
351 current = scope.killMode,
352 override = scope.killMode.copy(flushArtProfiles = true)
353 ) {
354 check(!scope.hasFlushedArtProfiles)
355 repeat(warmupIterations) { warmupBlock() }
356 scope.killProcess()
357 check(scope.hasFlushedArtProfiles) {
358 "Process $packageName never flushed profiles in any process - check that" +
359 " you launched the process, and that you only killed it with" +
360 " scope.killProcess, which will save profiles."
361 }
362 cmdPackageCompile(packageName, "speed-profile")
363 }
364 }
365 }
366
367 override fun shouldReset(): Boolean = true
368 }
369
370 /**
371 * Full ahead-of-time compilation of all method (but not classes) in the target application.
372 *
373 * Equates to `cmd package compile -f -m speed <package>` on API 24+.
374 *
375 * On Android M (API 23), this is the only supported compilation mode, as all apps are fully
376 * compiled ahead-of-time.
377 */
378 @Suppress("CanSealedSubClassBeObject") // Leaving possibility for future configuration
379 class Full : CompilationMode() {
380 override fun toString(): String = "Full"
381
382 override fun compileImpl(scope: MacrobenchmarkScope, warmupBlock: () -> Unit) {
383 if (Build.VERSION.SDK_INT >= 24) {
384 cmdPackageCompile(scope.packageName, "speed")
385 }
386 // Noop on older versions: apps are fully compiled at install time on API 23 and below
387 }
388
389 override fun shouldReset(): Boolean = true
390 }
391
392 /**
393 * No JIT / pre-compilation, all app code runs on the interpreter.
394 *
395 * Note: this mode will only be supported on rooted devices with jit disabled. For this reason,
396 * it's only available for internal benchmarking.
397 *
398 * TODO: migrate this to an internal-only flag on [None] instead
399 */
400 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
401 object Interpreted : CompilationMode() {
402 override fun toString(): String = "Interpreted"
403
404 override fun compileImpl(scope: MacrobenchmarkScope, warmupBlock: () -> Unit) {
405 // Nothing to do - handled externally
406 }
407
408 override fun shouldReset(): Boolean = true
409 }
410
411 companion object {
412
413 /**
414 * Represents the default compilation mode for the platform, on an end user's device.
415 *
416 * This is a post-store-install app configuration for this device's SDK
417 * version - [`Partial(BaselineProfileMode.UseIfAvailable)`][Partial] on API 24+, and [Full]
418 * prior to API 24 (where all apps are fully AOT compiled).
419 *
420 * On API 24+, Baseline Profile pre-compilation is used if possible, but no error will be
421 * thrown if installation fails.
422 *
423 * Generally, it is preferable to explicitly pass a compilation mode, such as
424 * [`Partial(BaselineProfileMode.Required)`][Partial] to avoid ambiguity, and e.g. validate
425 * an app's BaselineProfile can be correctly used.
426 */
427 @JvmField
428 val DEFAULT: CompilationMode =
429 if (Build.VERSION.SDK_INT >= 24) {
430 Partial(
431 baselineProfileMode = BaselineProfileMode.UseIfAvailable,
432 warmupIterations = 0
433 )
434 } else {
435 // API 23 is always fully compiled
436 Full()
437 }
438
439 @RequiresApi(24)
440 internal fun cmdPackageCompile(packageName: String, compileArgument: String) {
441 val stdout =
442 Shell.executeScriptCaptureStdout(
443 "cmd package compile -f -m $compileArgument $packageName"
444 )
445 check(stdout.trim() == "Success" || stdout.contains("PERFORMED")) {
446 "Failed to compile (out=$stdout)"
447 }
448 }
449
450 @RequiresApi(24)
451 internal fun cmdPackageCompileReset(packageName: String) {
452 // cmd package compile --reset returns a "Success" or a "Failure" to stdout.
453 // Rather than rely on exit codes which are not always correct, we
454 // specifically look for the work "Success" in stdout to make sure reset
455 // actually happened.
456 val output =
457 Shell.executeScriptCaptureStdout("cmd package compile --reset $packageName")
458
459 check(output.trim() == "Success" || output.contains("PERFORMED")) {
460 compileResetErrorString(packageName, output, DeviceInfo.isEmulator)
461 }
462 }
463
464 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // enable testing
465 fun compileResetErrorString(
466 packageName: String,
467 output: String,
468 isEmulator: Boolean
469 ): String {
470 return "Unable to reset compilation of $packageName (out=$output)." +
471 if (output.contains("could not be compiled") && isEmulator) {
472 " Try updating your emulator -" +
473 " see https://issuetracker.google.com/issue?id=251540646"
474 } else {
475 ""
476 }
477 }
478 }
479 }
480
481 /**
482 * Returns true if the CompilationMode can be run with the device's current VM settings.
483 *
484 * Used by jetpack-internal benchmarks to skip CompilationModes that would self-suppress.
485 */
486 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
CompilationModenull487 fun CompilationMode.isSupportedWithVmSettings(): Boolean {
488 // Only check for supportedVmSettings when CompilationMode.Interpreted is being requested.
489 // More context: b/248085179
490 val interpreted = this == CompilationMode.Interpreted
491 return if (interpreted) {
492 val getProp = Shell.getprop("dalvik.vm.extra-opts")
493 val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
494 // true if requires interpreted, false otherwise
495 vmRunningInterpretedOnly
496 } else {
497 true
498 }
499 }
500
assumeSupportedWithVmSettingsnull501 internal fun CompilationMode.assumeSupportedWithVmSettings() {
502 if (!isSupportedWithVmSettings()) {
503 throw AssumptionViolatedException(
504 when {
505 DeviceInfo.isRooted && this == CompilationMode.Interpreted ->
506 """
507 To run benchmarks with CompilationMode $this,
508 you must disable jit on your device with the following command:
509 `adb shell setprop dalvik.vm.extra-opts -Xusejit:false; adb shell stop; adb shell start`
510 """
511 .trimIndent()
512 DeviceInfo.isRooted && this != CompilationMode.Interpreted ->
513 """
514 To run benchmarks with CompilationMode $this,
515 you must enable jit on your device with the following command:
516 `adb shell setprop dalvik.vm.extra-opts \"\"; adb shell stop; adb shell start`
517 """
518 .trimIndent()
519 else ->
520 "You must toggle usejit on the VM to use CompilationMode $this, this requires" +
521 "rooting your device."
522 }
523 )
524 }
525 }
526