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