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.perfetto
18 
19 import android.os.Build
20 import android.util.JsonReader
21 import androidx.annotation.CheckResult
22 import androidx.annotation.RequiresApi
23 import androidx.annotation.RestrictTo
24 import androidx.benchmark.Outputs
25 import androidx.benchmark.Shell
26 import androidx.benchmark.ShellFile
27 import androidx.benchmark.UserFile
28 import androidx.benchmark.UserInfo
29 import androidx.benchmark.inMemoryTrace
30 import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
31 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
32 import androidx.test.platform.app.InstrumentationRegistry
33 import androidx.tracing.perfetto.handshake.PerfettoSdkHandshake
34 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ALREADY_ENABLED
35 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_MISSING
36 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
37 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
38 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_OTHER
39 import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_SUCCESS
40 import java.io.File
41 import java.io.StringReader
42 
43 /** Enables capturing a Perfetto trace */
44 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
45 @RequiresApi(23)
46 public class PerfettoCapture(
47     /**
48      * Bundled is available above API 28, but we default to using unbundled as well on API 29, as
49      * ProcessStatsConfig.scan_all_processes_on_start isn't supported on the bundled version.
50      */
51     unbundled: Boolean = Build.VERSION.SDK_INT <= 29
52 ) {
53 
54     private val helper: PerfettoHelper = PerfettoHelper(unbundled)
55 
56     fun isRunning() = helper.isRunning()
57 
58     /** Start collecting perfetto trace. */
59     fun start(config: PerfettoConfig) =
60         inMemoryTrace("start perfetto") {
61             // Write config proto to dir that shell can read
62             //     We use `.pb` even with textproto so we'll only ever have one file
63             val configProtoFile =
64                 if (UserInfo.currentUserId > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
65                     ShellFile.inTempDir("trace_config.pb")
66                 } else {
67                     UserFile.inOutputsDir("trace_config.pb")
68                 }
69 
70             try {
71                 inMemoryTrace("write config") { config.writeTo(configProtoFile) }
72                 inMemoryTrace("start perfetto process") {
73                     helper.startCollecting(configProtoFile.absolutePath, config.isTextProto)
74                 }
75             } finally {
76                 configProtoFile.delete()
77             }
78         }
79 
80     /**
81      * Stop collection, and record trace to the specified file path.
82      *
83      * @param destinationPath Absolute path to write perfetto trace to. Must be shell-writable, such
84      *   as result of `context.getExternalFilesDir(null)` or other similar `external` paths.
85      */
86     public fun stop(destinationPath: String) =
87         inMemoryTrace("stop perfetto") { helper.stopCollecting(destinationPath) }
88 
89     /**
90      * Enables Perfetto SDK tracing in the [PerfettoSdkConfig.targetPackage]
91      *
92      * @return a pair of [androidx.tracing.perfetto.handshake.protocol.ResultCode] and a
93      *   user-friendly message explaining the code
94      */
95     @RequiresApi(30) // TODO(234351579): Support API < 30
96     @CheckResult
97     fun enableAndroidxTracingPerfetto(config: PerfettoSdkConfig): Pair<Int, String> =
98         enableAndroidxTracingPerfetto(
99             targetPackage = config.targetPackage,
100             provideBinariesIfMissing = config.provideBinariesIfMissing,
101             isColdStartupTracing =
102                 when (config.processState) {
103                     InitialProcessState.Alive -> false
104                     InitialProcessState.NotAlive -> true
105                     InitialProcessState.Unknown -> Shell.isPackageAlive(config.targetPackage)
106                 }
107         )
108 
109     @RequiresApi(30) // TODO(234351579): Support API < 30
110     @CheckResult
111     /**
112      * Enables Perfetto SDK tracing in the [PerfettoSdkConfig.targetPackage]
113      *
114      * @return a pair of [androidx.tracing.perfetto.handshake.protocol.ResultCode] and a
115      *   user-friendly message explaining the code
116      */
117     private fun enableAndroidxTracingPerfetto(
118         targetPackage: String,
119         provideBinariesIfMissing: Boolean,
120         isColdStartupTracing: Boolean
121     ): Pair<Int, String> {
122         if (!isAbiSupported()) {
123             throw IllegalStateException("Unsupported ABI (${Build.SUPPORTED_ABIS.joinToString()})")
124         }
125 
126         // construct a handshake
127         val handshake =
128             PerfettoSdkHandshake(
129                 targetPackage = targetPackage,
130                 parseJsonMap = { jsonString: String ->
131                     sequence {
132                             JsonReader(StringReader(jsonString)).use { reader ->
133                                 reader.beginObject()
134                                 while (reader.hasNext()) yield(
135                                     reader.nextName() to reader.nextString()
136                                 )
137                                 reader.endObject()
138                             }
139                         }
140                         .toMap()
141                 },
142                 executeShellCommand = { cmd ->
143                     val (stdout, stderr) = Shell.executeScriptCaptureStdoutStderr(cmd)
144                     listOf(stdout, stderr)
145                         .filter { it.isNotBlank() }
146                         .joinToString(separator = System.lineSeparator())
147                 }
148             )
149 
150         // try without supplying external Perfetto SDK tracing binaries
151         val responseNoSideloading =
152             if (isColdStartupTracing) {
153                 handshake.enableTracingColdStart()
154             } else {
155                 handshake.enableTracingImmediate()
156             }
157 
158         // if required, retry by supplying external Perfetto SDK tracing binaries
159         val response =
160             if (
161                 responseNoSideloading.resultCode == RESULT_CODE_ERROR_BINARY_MISSING &&
162                     provideBinariesIfMissing
163             ) {
164                 val librarySource = constructLibrarySource()
165                 if (isColdStartupTracing) {
166                     // do not support persistent for now
167                     handshake.enableTracingColdStart(persistent = false, librarySource)
168                 } else {
169                     handshake.enableTracingImmediate(librarySource)
170                 }
171             } else {
172                 // no retry
173                 responseNoSideloading
174             }
175 
176         // process the response
177         val message =
178             when (response.resultCode) {
179                 0 ->
180                     "The broadcast to enable tracing was not received. This most likely means " +
181                         "that the app does not contain the `androidx.tracing.tracing-perfetto` " +
182                         "library as its dependency."
183                 RESULT_CODE_SUCCESS -> "Success"
184                 RESULT_CODE_ALREADY_ENABLED -> "Perfetto SDK already enabled."
185                 RESULT_CODE_ERROR_BINARY_MISSING ->
186                     binaryMissingResponseString(response.requiredVersion, response.message)
187                 RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH ->
188                     "Perfetto SDK binary mismatch. " +
189                         "Required version: ${response.requiredVersion}. " +
190                         "Error: ${response.message}."
191                 RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR ->
192                     "Perfetto SDK binary verification failed. " +
193                         "Required version: ${response.requiredVersion}. " +
194                         "Error: ${response.message}. " +
195                         "If working with an unreleased snapshot, ensure all modules are built " +
196                         "against the same snapshot (e.g. clear caches and rebuild)."
197                 RESULT_CODE_ERROR_OTHER ->
198                     if (responseNoSideloading.resultCode == RESULT_CODE_ERROR_BINARY_MISSING) {
199                         binaryMissingResponseString(
200                             responseNoSideloading.requiredVersion,
201                             response
202                                 .message // note: we're using the error from the sideloading attempt
203                         )
204                     } else {
205                         "Error: ${response.message}."
206                     }
207                 else -> throw RuntimeException("Unrecognized result code: ${response.resultCode}.")
208             }
209         return response.resultCode to message
210     }
211 
212     private fun binaryMissingResponseString(requiredVersion: String?, message: String?) =
213         "Perfetto SDK binary dependencies missing. " +
214             "Required version: $requiredVersion. " +
215             "Error: $message.\n" +
216             "To fix, declare the following dependency in your" +
217             " *benchmark* project (i.e. not the app under benchmark): " +
218             "\nandroidTestImplementation(" +
219             "\"androidx.tracing:tracing-perfetto-binary:$requiredVersion\")"
220 
221     private fun constructLibrarySource(): PerfettoSdkHandshake.LibrarySource {
222         val baseApk =
223             File(
224                 InstrumentationRegistry.getInstrumentation()
225                     .context
226                     .applicationInfo
227                     .publicSourceDir!!
228             )
229 
230         val mvTmpFileDstFile = { srcFile: File, dstFile: File ->
231             Shell.mkdir(dstFile.parentFile!!.path)
232             Shell.mv(srcFile.path, dstFile.path)
233         }
234 
235         return PerfettoSdkHandshake.LibrarySource.apkLibrarySource(
236             baseApk,
237             Outputs.dirUsableByAppAndShell,
238             mvTmpFileDstFile
239         )
240     }
241 
242     class PerfettoSdkConfig(
243         val targetPackage: String,
244         val processState: InitialProcessState,
245         val provideBinariesIfMissing: Boolean = true
246     ) {
247         /** State of process before tracing begins. */
248         enum class InitialProcessState {
249             /** will schedule tracing on next cold start */
250             NotAlive,
251 
252             /** enable tracing on the target process immediately */
253             Alive,
254 
255             /** trigger cold start vs running tracing based on a check if process is alive */
256             Unknown
257         }
258     }
259 }
260