1 /*
<lambda>null2  * Copyright 2022 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.annotation.SuppressLint
20 import android.os.Build
21 import android.util.Log
22 import androidx.annotation.RequiresApi
23 import androidx.annotation.RestrictTo
24 import androidx.benchmark.Arguments
25 import androidx.benchmark.Shell
26 import androidx.profileinstaller.ProfileInstallReceiver
27 import androidx.profileinstaller.ProfileInstaller
28 
29 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // temporary, revert
30 object ProfileInstallBroadcast {
31     private val receiverName = ProfileInstallReceiver::class.java.name
32 
33     /**
34      * Returns null on success, error string on suppress-able error, or throws if profileinstaller
35      * not up to date.
36      *
37      * Returned error strings aren't thrown, to let the calling function decide strictness.
38      */
39     fun installProfile(packageName: String): String? {
40         Log.d(TAG, "Profile Installer - Install profile")
41         // For baseline profiles, we trigger this broadcast to force the baseline profile to be
42         // installed synchronously
43         val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
44         // Use an explicit broadcast given the app was force-stopped.
45         when (val result = Shell.amBroadcast("-a $action $packageName/$receiverName")) {
46             null,
47             // 0 is returned by the platform by default, and also if no broadcast receiver
48             // receives the broadcast.
49             0 -> {
50                 return "The baseline profile install broadcast was not received. " +
51                     "This most likely means that the profileinstaller library is missing " +
52                     "from the target apk."
53             }
54             ProfileInstaller.RESULT_INSTALL_SUCCESS -> {
55                 return null // success!
56             }
57             ProfileInstaller.RESULT_ALREADY_INSTALLED -> {
58                 throw RuntimeException(
59                     "Unable to install baseline profile. This most likely means that the " +
60                         "latest version of the profileinstaller library is not being used. " +
61                         "Please use the latest profileinstaller library version " +
62                         "in the target app."
63                 )
64             }
65             ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
66                 val sdkInt = Build.VERSION.SDK_INT
67                 throw RuntimeException(
68                     if (sdkInt <= 23) {
69                         "Baseline profiles aren't supported on this device version," +
70                             " as all apps are fully ahead-of-time compiled."
71                     } else {
72                         "The device SDK version ($sdkInt) isn't supported" +
73                             " by the target app's copy of profileinstaller." +
74                             if (sdkInt in 31..33) {
75                                 " Please use profileinstaller `1.2.1`" +
76                                     " or newer for API 31-33 support"
77                             } else if (sdkInt >= 34) {
78                                 " Please use profileinstaller `1.4.0`" +
79                                     " or newer for API 34+ support"
80                             } else {
81                                 ""
82                             }
83                     }
84                 )
85             }
86             ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
87                 return "No baseline profile was found in the target apk."
88             }
89             ProfileInstaller.RESULT_NOT_WRITABLE,
90             ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
91             ProfileInstaller.RESULT_IO_EXCEPTION,
92             ProfileInstaller.RESULT_PARSE_EXCEPTION -> {
93                 throw RuntimeException("Baseline Profile wasn't successfully installed")
94             }
95             else -> {
96                 throw RuntimeException("unrecognized ProfileInstaller result code: $result")
97             }
98         }
99     }
100 
101     private fun nullResultErrorMessage(broadcastLabel: String, versionAdded: String): String =
102         "The $broadcastLabel broadcast was not received. " +
103             "This most likely means that the `androidx.profileinstaller` library is " +
104             "missing from the target app, or is too old. Please use `$versionAdded` or " +
105             "newer. For more information refer to the release notes at " +
106             "https://developer.android.com/jetpack/androidx/releases/profileinstaller."
107 
108     /**
109      * Uses skip files for avoiding interference from ProfileInstaller when using
110      * [CompilationMode.None].
111      *
112      * Operation name is one of `WRITE_SKIP_FILE` or `DELETE_SKIP_FILE`.
113      *
114      * Returned error strings aren't thrown, to let the calling function decide strictness.
115      */
116     fun skipFileOperation(
117         packageName: String,
118         @Suppress("SameParameterValue") operation: String
119     ): String? {
120         Log.d(TAG, "Profile Installer - Skip File Operation: $operation")
121         // Redefining constants here, because these are only defined in the latest alpha for
122         // ProfileInstaller.
123         // Use an explicit broadcast given the app was force-stopped.
124         val action = "androidx.profileinstaller.action.SKIP_FILE"
125         val operationKey = "EXTRA_SKIP_FILE_OPERATION"
126         val extras = "$operationKey $operation"
127         val result = Shell.amBroadcast("-a $action -e $extras $packageName/$receiverName")
128         return when {
129             result == null || result == 0 -> {
130                 // 0 is returned by the platform by default, and also if no broadcast receiver
131                 // receives the broadcast.
132                 nullResultErrorMessage(
133                     broadcastLabel = "baseline profile skip file",
134                     versionAdded = "1.2.0-alpha03",
135                 )
136             }
137             operation == "WRITE_SKIP_FILE" && result == 10 -> { // RESULT_INSTALL_SKIP_FILE_SUCCESS
138                 null // success!
139             }
140             operation == "DELETE_SKIP_FILE" && result == 11 -> { // RESULT_DELETE_SKIP_FILE_SUCCESS
141                 null // success!
142             }
143             else -> {
144                 throw RuntimeException("unrecognized ProfileInstaller result code: $result")
145             }
146         }
147     }
148 
149     /**
150      * Save any in-memory profile data in the target app to disk, so it can be used for compilation.
151      *
152      * Returned error strings aren't thrown, to let the calling function decide strictness.
153      */
154     @RequiresApi(24)
155     fun saveProfile(packageName: String): String? {
156         Log.d(TAG, "Profile Installer - Save Profile")
157         val action = "androidx.profileinstaller.action.SAVE_PROFILE"
158         return when (val result = Shell.amBroadcast("-a $action $packageName/$receiverName")) {
159             null,
160             0 -> {
161                 // 0 is returned by the platform by default, and also if no broadcast receiver
162                 // receives the broadcast. This can be because the package name specified is
163                 // incorrect or an old version of profile installer was used.
164                 nullResultErrorMessage(
165                     broadcastLabel = "save profile",
166                     versionAdded = "1.3.1",
167                 )
168             }
169             12 -> { // RESULT_SAVE_PROFILE_SIGNALLED
170                 // While this is observed to be fast for simple/sample apps,
171                 // this can take up significantly longer on large apps
172                 // especially on low end devices (see b/316082056)
173                 @Suppress("BanThreadSleep") Thread.sleep(1000)
174                 null // success!
175             }
176             else -> {
177                 // We don't bother supporting RESULT_SAVE_PROFILE_SKIPPED here,
178                 // since we already perform SDK_INT checks and use @RequiresApi(24)
179                 throw RuntimeException("unrecognized ProfileInstaller result code: $result")
180             }
181         }
182     }
183 
184     enum class Operation(val extraValue: String, val minimumVersion: String, val successCode: Int) {
185         DropShaderCache(
186             extraValue = "DROP_SHADER_CACHE",
187             minimumVersion = "1.3.0-alpha02",
188             successCode = ProfileInstaller.RESULT_BENCHMARK_OPERATION_SUCCESS
189         ),
190         SaveProfile(
191             extraValue = "SAVE_PROFILE",
192             minimumVersion = "1.5.0-alpha01",
193             successCode = ProfileInstaller.RESULT_SAVE_PROFILE_SIGNALLED
194         ),
195     }
196 
197     private fun benchmarkOperation(
198         packageName: String,
199         operation: Operation,
200         pid: Int = -1
201     ): String? {
202         Log.d(TAG, "Profile Installer - Benchmark Operation: ${operation.extraValue}")
203         // Redefining constants here, because these are only defined in the latest alpha for
204         // ProfileInstaller.
205         // Use an explicit broadcast given the app was force-stopped.
206         val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
207         val operationKey = "EXTRA_BENCHMARK_OPERATION"
208         val pidExtra =
209             if (pid != -1) {
210                 require(operation == Operation.SaveProfile)
211                 " --ei EXTRA_PID $pid"
212             } else {
213                 require(operation != Operation.SaveProfile)
214                 ""
215             }
216         val broadcastArguments =
217             "-a $action" +
218                 " -e $operationKey ${operation.extraValue}" +
219                 pidExtra +
220                 " $packageName/$receiverName"
221         return when (val result = Shell.amBroadcast(broadcastArguments)) {
222             null,
223             0,
224             16 -> { // BENCHMARK_OPERATION_UNKNOWN
225                 // 0 is returned by the platform by default, and also if no broadcast receiver
226                 // receives the broadcast.
227 
228                 // NOTE: may need to update this over time for different versions,
229                 // based on operation string
230                 "The ${operation.extraValue} broadcast was not received. " +
231                     "This most likely means that the `androidx.profileinstaller` library " +
232                     "used by the target apk is old. Please use `${operation.minimumVersion}`" +
233                     " or newer. For more information refer to the release notes at " +
234                     "https://developer.android.com/jetpack/androidx/releases/profileinstaller. " +
235                     "If you are already using 'androidx.profileinstaller' library and still seeing " +
236                     "error, verify: 1) androidx.profileinstaller.ProfileInstallReceiver appears " +
237                     "unobfuscated in your APK's AndroidManifest and dex, and 2) the following " +
238                     "command executes successfully (should print ${operation.successCode}}): " +
239                     "adb shell am broadcast $broadcastArguments"
240             }
241             15 -> { // RESULT_BENCHMARK_OPERATION_FAILURE
242                 "The $operation broadcast failed."
243             }
244             else -> {
245                 if (result == operation.successCode) {
246                     null // success!
247                 } else {
248                     throw RuntimeException("unrecognized ProfileInstaller result code: $result")
249                 }
250             }
251         }
252     }
253 
254     fun dropShaderCache(packageName: String): String? =
255         benchmarkOperation(packageName, Operation.DropShaderCache)
256 
257     data class SaveProfileResult(val processCount: Int, val error: String?) {
258         init {
259             require(error == null || processCount > 0) {
260                 "Error only valid if processes are found running," +
261                     " error = $error, processCount = $processCount"
262             }
263         }
264     }
265 
266     @SuppressLint("BanThreadSleep")
267     @RequiresApi(24)
268     fun saveProfilesForAllProcesses(packageName: String): SaveProfileResult {
269         val processes = Shell.getRunningPidsAndProcessesForPackage(packageName)
270         processes
271             .sortedBy { it.processName }
272             .forEach { runningProcess ->
273                 Log.d(TAG, "Saving profiles for process $runningProcess")
274                 if (runningProcess.processName.contains(":")) {
275                     // Only attempt the new broadcast on processes that require it -
276                     // processes that aren't the main registered process
277                     // this lets single process apps run with profileinstaller 1.3/1.4
278                     // without needing the save profile operation.
279                     // NOTE: we are assuming here that the target doesn't move
280                     // ProfileInstallReceiver to a different process for simplicity
281                     val error =
282                         benchmarkOperation(
283                             packageName = packageName,
284                             operation = Operation.SaveProfile,
285                             pid = runningProcess.pid
286                         )
287                     if (error != null) return SaveProfileResult(processes.size, error)
288                     Thread.sleep(Arguments.saveProfileWaitMillis)
289                 } else {
290                     val error = saveProfile(packageName)
291                     if (error != null) return SaveProfileResult(processes.size, error)
292                 }
293             }
294         return SaveProfileResult(processes.size, null) // success!
295     }
296 }
297