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