blob: 5d5e266a8a27e7dcff69dd52ba9046d02b5d7c66 [file] [log] [blame]
/*
* 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.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.DeviceInfo.deviceSummaryString
import androidx.benchmark.Shell
import androidx.benchmark.userspaceTrace
import androidx.test.platform.app.InstrumentationRegistry
import org.jetbrains.annotations.TestOnly
import java.io.File
import java.io.IOException
/**
* PerfettoHelper is used to start and stop the perfetto tracing and move the
* output perfetto trace file to destination folder.
*
* @suppress
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(21)
public class PerfettoHelper(
private val unbundled: Boolean = Build.VERSION.SDK_INT < LOWEST_BUNDLED_VERSION_SUPPORTED
) {
init {
require(unbundled || Build.VERSION.SDK_INT >= LOWEST_BUNDLED_VERSION_SUPPORTED) {
"Perfetto capture using the os version of perfetto requires API " +
"$LOWEST_BUNDLED_VERSION_SUPPORTED or greater."
}
}
var perfettoPid: Int? = null
private fun perfettoStartupException(label: String, cause: Exception?): IllegalStateException {
return IllegalStateException(
"""
$label
Please report a bug, and include a logcat capture of the test run and failure.
$deviceSummaryString
""".trimIndent(),
cause
)
}
/**
* Start the perfetto tracing in background using the given config file.
*
* The output will be written to /data/misc/perfetto-traces/trace_output.pb. Perfetto has
* write access only to /data/misc/perfetto-traces/ folder. The config file may be anywhere
* readable by shell.
*
* @param configFilePath used for collecting the perfetto trace.
* @param isTextProtoConfig true if the config file is textproto format otherwise false.
*/
public fun startCollecting(configFilePath: String, isTextProtoConfig: Boolean) {
require(configFilePath.isNotEmpty()) {
"Perfetto config cannot be empty."
}
require(perfettoPid == null) {
"Perfetto instance is already running"
}
try {
// Cleanup already existing perfetto process.
Log.i(LOG_TAG, "Cleanup perfetto before starting.")
stopAllPerfettoProcesses()
// The actual location of the config path.
val actualConfigPath = if (unbundled) {
val path = "$UNBUNDLED_PERFETTO_ROOT_DIR/config.pb"
// Move the config to a directory that unbundled perfetto has permissions for.
Shell.executeCommand("rm $path")
Shell.executeCommand("mv $configFilePath $path")
path
} else {
configFilePath
}
val outputPath = getPerfettoTmpOutputFilePath()
// Remove already existing temporary output trace file if any.
val output = Shell.executeCommand("rm $outputPath")
Log.i(LOG_TAG, "Perfetto output file cleanup - $output")
// Perfetto
val perfettoCmd = perfettoCommand(actualConfigPath, isTextProtoConfig)
Log.i(LOG_TAG, "Starting perfetto tracing with cmd: $perfettoCmd")
val perfettoCmdOutput = Shell.executeScript(perfettoCmd).trim()
Log.i(LOG_TAG, "Perfetto pid - $perfettoCmdOutput")
perfettoPid = perfettoCmdOutput.toInt()
} catch (ioe: IOException) {
throw perfettoStartupException("Unable to start perfetto tracing", ioe)
}
if (!isRunning()) {
throw perfettoStartupException("Perfetto tracing failed to start.", null)
}
Log.i(LOG_TAG, "Perfetto tracing started successfully with pid $perfettoPid.")
}
/**
* Check if this PerfettoHelper's perfetto process is running or not.
*
* @return true if perfetto is running otherwise false.
*/
public fun isRunning(): Boolean {
return perfettoPid?.let {
Shell.isProcessAlive(it, perfettoProcessName)
} ?: false
}
/**
* Stop the perfetto trace collection under /data/misc/perfetto-traces/trace_output.pb after
* waiting for given time in msecs and copy the output to the destination file.
*
* @param waitTimeInMsecs time to wait in msecs before stopping the trace collection.
* @param destinationFile file to copy the perfetto output trace.
* @return true if the trace collection is successful otherwise false.
*/
public fun stopCollecting(waitTimeInMsecs: Long, destinationFile: String) {
// Wait for the dump interval before stopping the trace.
userspaceTrace("Wait for perfetto flush") {
Log.i(LOG_TAG, "Waiting for $waitTimeInMsecs millis before stopping perfetto.")
SystemClock.sleep(waitTimeInMsecs)
}
// Stop the perfetto and copy the output file.
Log.i(LOG_TAG, "Stopping perfetto.")
userspaceTrace("stop perfetto process") {
stopPerfetto()
}
Log.i(LOG_TAG, "Writing to $destinationFile.")
userspaceTrace("copy trace to output dir") {
copyFileOutput(destinationFile)
}
}
/**
* Utility method for stopping perfetto.
*
* @return true if perfetto is stopped successfully.
*/
private fun stopPerfetto() {
val pid = perfettoPid
require(pid != null)
Shell.terminateProcessesAndWait(
waitPollPeriodMs = PERFETTO_KILL_WAIT_TIME_MS,
waitPollMaxCount = PERFETTO_KILL_WAIT_COUNT,
Shell.ProcessPid(
pid = pid,
processName = perfettoProcessName
)
)
perfettoPid = null
}
/**
* @return the shell command that can be used to start Perfetto.
*/
private fun perfettoCommand(configFilePath: String, isTextProtoConfig: Boolean): String {
val outputPath = getPerfettoTmpOutputFilePath()
var command = if (!unbundled) (
// Bundled perfetto reads configuration from stdin.
"cat $configFilePath | perfetto --background -c - -o $outputPath"
) else {
// Unbundled perfetto can read configuration from a file that it has permissions to
// read from. This because it assumes the identity of the shell and therefore has
// access to /data/local/tmp directory.
"$unbundledPerfettoShellPath --background" +
" -c $configFilePath" +
" -o $outputPath"
}
if (isTextProtoConfig) {
command += PERFETTO_TXT_PROTO_ARG
}
return command
}
/**
* @return the [String] path to the temporary output file used to store the trace file
* during collection.
*/
private fun getPerfettoTmpOutputFilePath(): String {
return if (unbundled) {
UNBUNDLED_TEMP_OUTPUT_FILE
} else {
PERFETTO_TMP_OUTPUT_FILE
}
}
/**
* Copy the temporary perfetto trace output file from /data/local/tmp/trace_output.pb to given
* destinationFile.
*
* @param destinationFile file to copy the perfetto output trace.
* @return true if the trace file copied successfully otherwise false.
*/
private fun copyFileOutput(destinationFile: String): Boolean {
val sourceFile = getPerfettoTmpOutputFilePath()
val filePath = File(destinationFile)
val destDirectory = filePath.parent
if (destDirectory != null) {
// Check if the directory already exists
val directory = File(destDirectory)
if (!directory.exists()) {
val success = directory.mkdirs()
if (!success) {
Log.e(
LOG_TAG,
"Result output directory $destDirectory not created successfully."
)
return false
}
}
}
// Copy the collected trace from /data/misc/perfetto-traces/trace_output.pb to
// destinationFile
try {
val moveResult =
Shell.executeCommand("mv $sourceFile $destinationFile")
if (moveResult.isNotEmpty()) {
Log.e(
LOG_TAG,
"""
Unable to move perfetto output file from $sourceFile
to $destinationFile due to $moveResult.
""".trimIndent()
)
return false
}
} catch (ioe: IOException) {
Log.e(
LOG_TAG,
"Unable to move the perfetto trace file to destination file.",
ioe
)
return false
}
return true
}
// Perfetto executable
private val perfettoProcessName = if (unbundled) "tracebox" else "perfetto"
companion object {
internal const val LOG_TAG = "PerfettoCapture"
const val LOWEST_BUNDLED_VERSION_SUPPORTED = 29
// Command to start the perfetto tracing in the background.
// perfetto --background -c /data/misc/perfetto-traces/trace_config.pb -o
// /data/misc/perfetto-traces/trace_output.pb
private const val PERFETTO_TMP_OUTPUT_FILE = "/data/misc/perfetto-traces/trace_output.pb"
// Additional arg to indicate that the perfetto config file is text format.
private const val PERFETTO_TXT_PROTO_ARG = " --txt"
// Max wait count for checking if perfetto is stopped successfully
private const val PERFETTO_KILL_WAIT_COUNT = 30
// Check if perfetto is stopped every 100 millis.
private const val PERFETTO_KILL_WAIT_TIME_MS: Long = 500
// Path where unbundled tracebox is copied to
private const val UNBUNDLED_PERFETTO_ROOT_DIR = "/data/local/tmp"
private const val UNBUNDLED_TEMP_OUTPUT_FILE =
"$UNBUNDLED_PERFETTO_ROOT_DIR/trace_output.pb"
// A set of supported ABIs
private val SUPPORTED_64_ABIS = setOf("arm64-v8a", "x86_64")
private val SUPPORTED_32_ABIS = setOf("armeabi")
@TestOnly
fun isAbiSupported(): Boolean {
Log.d(LOG_TAG, "Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}")
// Cuttlefish is x86 but claims support for x86_64
return !Build.MODEL.contains("Cuttlefish") && ( // b/180022458
Build.SUPPORTED_64_BIT_ABIS.any { SUPPORTED_64_ABIS.contains(it) } ||
Build.SUPPORTED_32_BIT_ABIS.any { SUPPORTED_32_ABIS.contains(it) }
)
}
@get:TestOnly
val unbundledPerfettoShellPath: String by lazy {
createExecutable("tracebox")
}
fun createExecutable(tool: String): String {
userspaceTrace("create executable: $tool") {
if (!isAbiSupported()) {
throw IllegalStateException(
"Unsupported ABI (${Build.SUPPORTED_ABIS.joinToString()})"
)
}
val suffix = when {
// The order is important because `SUPPORTED_64_BIT_ABIS` lists all ABI
// supported by a device. That is why we need to search from most specific to
// least specific. For e.g. emulators claim to support aarch64, when in reality
// they can only support x86 or x86_64.
Build.SUPPORTED_64_BIT_ABIS.any { it.startsWith("x86_64") } -> "x86_64"
Build.SUPPORTED_64_BIT_ABIS.any { it.startsWith("arm64") } -> "aarch64"
Build.SUPPORTED_32_BIT_ABIS.any { it.startsWith("armeabi") } -> "arm"
else -> IllegalStateException(
// Perfetto does not support x86 binaries
"Unsupported ABI (${Build.SUPPORTED_ABIS.joinToString()})"
)
}
val instrumentation = InstrumentationRegistry.getInstrumentation()
val inputStream = instrumentation.context.assets.open("${tool}_$suffix")
return Shell.createRunnableExecutable(tool, inputStream)
}
}
public fun stopAllPerfettoProcesses() {
listOf("perfetto", "tracebox").forEach { processName ->
Shell.terminateProcessesAndWait(
waitPollPeriodMs = PERFETTO_KILL_WAIT_TIME_MS,
waitPollMaxCount = PERFETTO_KILL_WAIT_COUNT,
processName
)
}
}
}
}