PerfettoSdkTrace.kt

/*
 * Copyright 2022 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.tracing.perfetto

import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.tracing.perfetto.internal.handshake.protocol.Response
import androidx.tracing.perfetto.internal.handshake.protocol.ResponseResultCodes.RESULT_CODE_ALREADY_ENABLED
import androidx.tracing.perfetto.internal.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_MISSING
import androidx.tracing.perfetto.internal.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR
import androidx.tracing.perfetto.internal.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH
import androidx.tracing.perfetto.internal.handshake.protocol.ResponseResultCodes.RESULT_CODE_ERROR_OTHER
import androidx.tracing.perfetto.internal.handshake.protocol.ResponseResultCodes.RESULT_CODE_SUCCESS
import androidx.tracing.perfetto.jni.PerfettoNative
import androidx.tracing.perfetto.security.IncorrectChecksumException
import androidx.tracing.perfetto.security.SafeLibLoader
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock

/** Allows for emitting trace events using Perfetto SDK. */
object PerfettoSdkTrace {
    /**
     * Checks whether the tracing library has been loaded and the app has been registered with
     * Perfetto SDK tracing server. This is useful to avoid intermediate string creation for trace
     * sections that require formatting. It is not necessary to guard all Trace method calls as they
     * internally already check this. However it is recommended to use this to prevent creating any
     * temporary objects that would then be passed to those methods to reduce runtime cost when
     * tracing isn't enabled.
     *
     * @return true if tracing is currently enabled, false otherwise
     */
    // Note: some of class' code relies on the field never changing from true -> false,
    // which is realistic (at the time of writing this, we are unable to unload the library and
    // unregister the app with Perfetto).
    var isEnabled: Boolean = false
        private set

    /**
     * Ensures that we enable tracing (load the tracing library and register with Perfetto) only
     * once.
     *
     * Note: not intended for synchronization during tracing as not to impact performance.
     */
    private val enableTracingLock = ReentrantReadWriteLock()

    @RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
    internal fun enable() = enable(null)

    @RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
    internal fun enable(file: File, context: Context) = enable(file to context)

    @RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
    private fun enable(descriptor: Pair<File, Context>?): Response {
        enableTracingLock.readLock().withLock {
            if (isEnabled) return Response(RESULT_CODE_ALREADY_ENABLED)
        }

        enableTracingLock.writeLock().withLock {
            return enableImpl(descriptor)
        }
    }

    /** Calling thread must obtain a write lock on [enableTracingLock] before calling this method */
    @RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
    private fun enableImpl(descriptor: Pair<File, Context>?): Response {
        if (!enableTracingLock.isWriteLockedByCurrentThread) throw RuntimeException()

        if (isEnabled) return Response(RESULT_CODE_ALREADY_ENABLED)

        // Load library
        try {
            when (descriptor) {
                null -> PerfettoNative.loadLib()
                else -> descriptor.let { (file, context) ->
                    PerfettoNative.loadLib(file, SafeLibLoader(context))
                }
            }
        } catch (t: Throwable) {
            return when (t) {
                is IncorrectChecksumException ->
                    Response(RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR, t)
                is UnsatisfiedLinkError ->
                    Response(RESULT_CODE_ERROR_BINARY_MISSING, t)
                is Exception ->
                    Response(RESULT_CODE_ERROR_OTHER, t)
                else -> throw t
            }
        }

        // Verify binary/java version match
        val nativeVersion = PerfettoNative.nativeVersion()
        val javaVersion = PerfettoNative.Metadata.version
        if (nativeVersion != javaVersion) {
            return Response(
                RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH,
                "Binary and Java version mismatch. Binary: $nativeVersion. Java: $javaVersion"
            )
        }

        // Register as a Perfetto SDK data-source
        try {
            PerfettoNative.nativeRegisterWithPerfetto()
        } catch (e: Exception) {
            return Response(RESULT_CODE_ERROR_OTHER, e)
        }

        isEnabled = true
        return Response(RESULT_CODE_SUCCESS)
    }

    /**
     * Writes a trace message to indicate that a given section of code has begun. This call must
     * be followed by a corresponding call to [endSection] on the same thread.
     *
     * @param sectionName The name of the code section to appear in the trace.
     */
    fun beginSection(sectionName: String) {
        if (isEnabled) {
            // Note: key is not currently used, so passing 0 for now
            PerfettoNative.nativeTraceEventBegin(key = 0, traceInfo = sectionName)
        }
    }

    /**
     * Writes a trace message to indicate that a given section of code has ended. This call must
     * be preceded by a corresponding call to [beginSection]. Calling this method
     * will mark the end of the most recently begun section of code, so care must be taken to
     * ensure that [beginSection] / [endSection] pairs are properly nested and called from the same
     * thread.
     */
    fun endSection() {
        if (isEnabled) PerfettoNative.nativeTraceEventEnd()
    }

    private fun errorMessage(t: Throwable): String = t.run {
        javaClass.name + if (message != null) ": $message" else ""
    }

    internal fun Response(resultCode: Int, message: String? = null) =
        Response(resultCode, PerfettoNative.Metadata.version, message)

    internal fun Response(resultCode: Int, exception: Throwable) =
        Response(resultCode, errorMessage(exception))
}