PerfettoCaptureWrapper.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.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.benchmark.InMemoryTracing
import androidx.benchmark.Outputs
import androidx.benchmark.Outputs.dateToFileName
import androidx.benchmark.PropOverride
import androidx.benchmark.Shell
import androidx.benchmark.perfetto.PerfettoHelper.Companion.LOG_TAG
import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_ALREADY_ENABLED
import androidx.tracing.perfetto.handshake.protocol.ResponseResultCodes.RESULT_CODE_SUCCESS
import java.io.FileOutputStream
import java.lang.RuntimeException

/**
 * Wrapper for [PerfettoCapture] which does nothing below API 23.
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class PerfettoCaptureWrapper {
    private var capture: PerfettoCapture? = null
    private val TRACE_ENABLE_PROP = "persist.traced.enable"

    init {
        if (Build.VERSION.SDK_INT >= 23) {
            capture = PerfettoCapture()
        }
    }

    companion object {
        val inUseLock = Object()

        /**
         * Prevents re-entrance of perfetto trace capture, as it doesn't handle this correctly
         *
         * (Single file output location, process cleanup, etc.)
         */
        var inUse = false
    }

    @RequiresApi(23)
    private fun start(
        config: PerfettoConfig,
        perfettoSdkConfig: PerfettoCapture.PerfettoSdkConfig?
    ): Boolean {
        capture?.apply {
            Log.d(LOG_TAG, "Recording perfetto trace")
            if (perfettoSdkConfig != null &&
                Build.VERSION.SDK_INT >= 30
            ) {
                val (resultCode, message) = enableAndroidxTracingPerfetto(perfettoSdkConfig)
                Log.d(LOG_TAG, "Enable full tracing result=$message")

                if (resultCode !in arrayOf(RESULT_CODE_SUCCESS, RESULT_CODE_ALREADY_ENABLED)) {
                    throw RuntimeException(
                        "Issue while enabling Perfetto SDK tracing in" +
                            " ${perfettoSdkConfig.targetPackage}: $message"
                    )
                }
            }
            start(config)
        }

        return true
    }

    @RequiresApi(23)
    private fun stop(traceLabel: String): String {
        return Outputs.writeFile(
            fileName = "${traceLabel}_${dateToFileName()}.perfetto-trace"
        ) {
            capture!!.stop(it.absolutePath)
            if (Outputs.forceFilesForShellAccessible) {
                // This shell written file must be made readable to be later accessed by this
                // process (e.g. for appending UiState). Unlike in other places, shell
                // must increase access, since it's giving the app access
                Shell.executeScriptSilent("chmod 777 ${it.absolutePath}")
            }
        }
    }

    fun record(
        fileLabel: String,
        config: PerfettoConfig,
        perfettoSdkConfig: PerfettoCapture.PerfettoSdkConfig?,
        traceCallback: ((String) -> Unit)? = null,
        enableTracing: Boolean = true,
        inMemoryTracingLabel: String? = null,
        block: () -> Unit
    ): String? {
        // skip if Perfetto not supported, or if caller opts out
        if (Build.VERSION.SDK_INT < 23 || !isAbiSupported() || !enableTracing) {
            block()
            return null
        }

        synchronized(inUseLock) {
            if (inUse) {
                throw IllegalStateException(
                    "Reentrant Perfetto Tracing is not supported." +
                        " This means you cannot use more than one of" +
                        " BenchmarkRule/MacrobenchmarkRule/PerfettoTraceRule/PerfettoTrace.record" +
                        " together."
                )
            }
            inUse = true
        }
        // Prior to Android 11 (R), a shell property must be set to enable perfetto tracing, see
        // https://perfetto.dev/docs/quickstart/android-tracing#starting-the-tracing-services
        val propOverride = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
            PropOverride(TRACE_ENABLE_PROP, "1")
        } else null

        val path: String
        try {
            propOverride?.forceValue()
            start(config, perfettoSdkConfig)

            // To avoid b/174007010, userspace tracing is cleared and saved *during* trace, so
            // that events won't lie outside the bounds of the trace content.
            InMemoryTracing.clearEvents()
            try {
                block()
            } finally {
                // finally here to ensure trace is fully recorded if block throws
                path = stop(fileLabel)

                if (inMemoryTracingLabel != null) {
                    val inMemoryTrace = InMemoryTracing.commitToTrace(inMemoryTracingLabel)
                    inMemoryTrace.encode(FileOutputStream(path, /* append = */ true))
                }
                traceCallback?.invoke(path)
            }
            return path
        } finally {
            propOverride?.resetIfOverridden()
            synchronized(inUseLock) {
                inUse = false
            }
        }
    }
}