PerfettoCapture.kt
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.benchmark.perfetto
import android.os.Build
import android.util.JsonReader
import androidx.annotation.CheckResult
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.Outputs
import androidx.benchmark.Shell
import androidx.benchmark.inMemoryTrace
import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.test.platform.app.InstrumentationRegistry
import androidx.tracing.perfetto.handshake.PerfettoSdkHandshake
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ALREADY_ENABLED
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_MISSING
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_OTHER
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_SUCCESS
import java.io.File
import java.io.StringReader
/**
* Enables capturing a Perfetto trace
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(23)
public class PerfettoCapture(
/**
* Bundled is available above API 28, but we default to using unbundled as well on API 29, as
* ProcessStatsConfig.scan_all_processes_on_start isn't supported on the bundled version.
*/
unbundled: Boolean = Build.VERSION.SDK_INT <= 29
) {
private val helper: PerfettoHelper = PerfettoHelper(unbundled)
fun isRunning() = helper.isRunning()
/**
* Start collecting perfetto trace.
*/
fun start(config: PerfettoConfig) = inMemoryTrace("start perfetto") {
// Write config proto to dir that shell can read
// We use `.pb` even with textproto so we'll only ever have one file
val configProtoFile = File(Outputs.dirUsableByAppAndShell, "trace_config.pb")
try {
inMemoryTrace("write config") {
config.writeTo(configProtoFile)
if (Outputs.forceFilesForShellAccessible) {
configProtoFile.setReadable(true, /* ownerOnly = */ false)
}
}
inMemoryTrace("start perfetto process") {
helper.startCollecting(configProtoFile.absolutePath, config.isTextProto)
}
} finally {
configProtoFile.delete()
}
}
/**
* Stop collection, and record trace to the specified file path.
*
* @param destinationPath Absolute path to write perfetto trace to. Must be shell-writable,
* such as result of `context.getExternalFilesDir(null)` or other similar `external` paths.
*/
public fun stop(destinationPath: String) = inMemoryTrace("stop perfetto") {
helper.stopCollecting(destinationPath)
}
/**
* Enables Perfetto SDK tracing in the [PerfettoSdkConfig.targetPackage]
*
* @return a pair of [androidx.tracing.perfetto.handshake.protocol.ResultCode] and
* a user-friendly message explaining the code
*/
@RequiresApi(30) // TODO(234351579): Support API < 30
@CheckResult
fun enableAndroidxTracingPerfetto(config: PerfettoSdkConfig): Pair<Int, String> =
enableAndroidxTracingPerfetto(
targetPackage = config.targetPackage,
provideBinariesIfMissing = config.provideBinariesIfMissing,
isColdStartupTracing = when (config.processState) {
InitialProcessState.Alive -> false
InitialProcessState.NotAlive -> true
InitialProcessState.Unknown -> Shell.isPackageAlive(config.targetPackage)
}
)
@RequiresApi(30) // TODO(234351579): Support API < 30
@CheckResult
/**
* Enables Perfetto SDK tracing in the [PerfettoSdkConfig.targetPackage]
*
* @return a pair of [androidx.tracing.perfetto.handshake.protocol.ResultCode] and
* a user-friendly message explaining the code
*/
private fun enableAndroidxTracingPerfetto(
targetPackage: String,
provideBinariesIfMissing: Boolean,
isColdStartupTracing: Boolean
): Pair<Int, String> {
if (!isAbiSupported()) {
throw IllegalStateException("Unsupported ABI (${Build.SUPPORTED_ABIS.joinToString()})")
}
// construct a handshake
val handshake = PerfettoSdkHandshake(
targetPackage = targetPackage,
parseJsonMap = { jsonString: String ->
sequence {
JsonReader(StringReader(jsonString)).use { reader ->
reader.beginObject()
while (reader.hasNext()) yield(reader.nextName() to reader.nextString())
reader.endObject()
}
}.toMap()
},
executeShellCommand = { cmd ->
val (stdout, stderr) = Shell.executeScriptCaptureStdoutStderr(cmd)
listOf(stdout, stderr).filter { it.isNotBlank() }.joinToString(
separator = System.lineSeparator()
)
}
)
// try without supplying external Perfetto SDK tracing binaries
val responseNoSideloading = if (isColdStartupTracing) {
handshake.enableTracingColdStart()
} else {
handshake.enableTracingImmediate()
}
// if required, retry by supplying external Perfetto SDK tracing binaries
val response = if (responseNoSideloading.resultCode == RESULT_CODE_ERROR_BINARY_MISSING &&
provideBinariesIfMissing
) {
val librarySource = constructLibrarySource()
if (isColdStartupTracing) {
// do not support persistent for now
handshake.enableTracingColdStart(persistent = false, librarySource)
} else {
handshake.enableTracingImmediate(librarySource)
}
} else {
// no retry
responseNoSideloading
}
// process the response
val message = when (response.resultCode) {
0 -> "The broadcast to enable tracing was not received. This most likely means " +
"that the app does not contain the `androidx.tracing.tracing-perfetto` " +
"library as its dependency."
RESULT_CODE_SUCCESS -> "Success"
RESULT_CODE_ALREADY_ENABLED -> "Perfetto SDK already enabled."
RESULT_CODE_ERROR_BINARY_MISSING ->
binaryMissingResponseString(response.requiredVersion, response.message)
RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH ->
"Perfetto SDK binary mismatch. " +
"Required version: ${response.requiredVersion}. " +
"Error: ${response.message}."
RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR ->
"Perfetto SDK binary verification failed. " +
"Required version: ${response.requiredVersion}. " +
"Error: ${response.message}. " +
"If working with an unreleased snapshot, ensure all modules are built " +
"against the same snapshot (e.g. clear caches and rebuild)."
RESULT_CODE_ERROR_OTHER ->
if (responseNoSideloading.resultCode == RESULT_CODE_ERROR_BINARY_MISSING) {
binaryMissingResponseString(
responseNoSideloading.requiredVersion,
response.message // note: we're using the error from the sideloading attempt
)
} else {
"Error: ${response.message}."
}
else -> throw RuntimeException("Unrecognized result code: ${response.resultCode}.")
}
return response.resultCode to message
}
private fun binaryMissingResponseString(requiredVersion: String?, message: String?) =
"Perfetto SDK binary dependencies missing. " +
"Required version: $requiredVersion. " +
"Error: $message.\n" +
"To fix, declare the following dependency in your" +
" *benchmark* project (i.e. not the app under benchmark): " +
"\nandroidTestImplementation(" +
"\"androidx.tracing:tracing-perfetto-binary:$requiredVersion\")"
private fun constructLibrarySource(): PerfettoSdkHandshake.LibrarySource {
val baseApk = File(
InstrumentationRegistry.getInstrumentation().context.applicationInfo.publicSourceDir!!
)
val mvTmpFileDstFile = { srcFile: File, dstFile: File ->
Shell.executeScriptSilent("mkdir -p ${dstFile.parentFile!!.path}")
Shell.executeScriptSilent("mv ${srcFile.path} ${dstFile.path}")
}
return PerfettoSdkHandshake.LibrarySource.apkLibrarySource(
baseApk,
Outputs.dirUsableByAppAndShell,
mvTmpFileDstFile
)
}
class PerfettoSdkConfig(
val targetPackage: String,
val processState: InitialProcessState,
val provideBinariesIfMissing: Boolean = true
) {
/** State of process before tracing begins. */
enum class InitialProcessState {
/** will schedule tracing on next cold start */
NotAlive,
/** enable tracing on the target process immediately */
Alive,
/** trigger cold start vs running tracing based on a check if process is alive */
Unknown
}
}
}