Seguimiento en proceso

La biblioteca androidx.tracing:tracing:2.0.0-beta01 es una API de Kotlin de baja sobrecarga que te permite capturar eventos de seguimiento en el proceso. Estos eventos pueden capturar segmentos de tiempo y su contexto. La biblioteca también admite la propagación del contexto para las corrutinas de Kotlin.

La biblioteca usa el mismo Perfetto formato de paquete de seguimiento que conocen los desarrolladores de Android. Además, Tracing 2.0 (a diferencia de las APIs 1.0.0-*) admite la noción de backends de seguimiento conectables y receptores, por lo que otras bibliotecas de seguimiento pueden personalizar el formato de seguimiento de salida y cómo funciona la propagación del contexto en su implementación.

Dependencias

Para comenzar el seguimiento, debes definir las dependencias en build.gradle.kts.

Proyectos de Kotlin Multiplatform

Las bibliotecas que solo necesitan emitir eventos de seguimiento deben depender de la API ligera androidx.tracing:tracing. Las apps que configuran el backend de seguimiento también deben depender de androidx.tracing:tracing-wire.

kotlin {
  sourceSets {
    commonMain {
      dependencies {
        // API definition
        implementation("androidx.tracing:tracing:2.0.0-beta01")
      }
    }
    androidMain {
      dependencies {
        // Android implementation (includes the Perfetto Sink and automatic initialization)
        implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
      }
    }
    jvmMain {
      dependencies {
        // JVM implementation
        implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
      }
    }
  }
}

Proyectos solo para Android

Si solo segmentas Android, agrega lo siguiente al archivo build.gradle.kts de tu aplicación o biblioteca:

dependencies {
    // For libraries and applications to emit events
    implementation("androidx.tracing:tracing:2.0.0-beta01")

    // For applications to configure the tracing backend
    implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}

Inicialización y descubrimiento

Antes de que puedas registrar eventos de seguimiento, debes inicializar la infraestructura de seguimiento. Esto implica crear un AbstractTraceDriver y registrar su Tracer de forma global.

Android

En Android, si incluyes la dependencia androidx.tracing:tracing-wire, la inicialización se realiza automáticamente cuando se inicia la aplicación con la biblioteca androidx.startup.

De forma predeterminada, esta inicialización automática hace lo siguiente:

  • Crea un TraceDriver con un TraceSink que escribe archivos de seguimiento de Perfetto en Context.noBackupFilesDir/perfetto_traces/.

  • Registra el Tracer resultante de forma global.

Personaliza la instancia TraceDriver

Si necesitas personalizar la configuración, por ejemplo, para cambiar la ubicación en la que se guardan los archivos de seguimiento o usar un TraceSink personalizado, puedes proporcionar tu propia instancia AbstractTraceDriver.

Para personalizar la configuración, haz que tu clase Application implemente AbstractTraceDriver.Factory:

import android.app.Application
import androidx.tracing.AbstractTraceDriver
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File

class App : Application(), AbstractTraceDriver.Factory {
    override fun create(): AbstractTraceDriver {
        val sink = TraceSink(
            context = this,
            fileProvider = { File(noBackupFilesDir, "traces") },
        )
        // Return the custom TraceDriver
        // You can also fully customize the instance of Tracer
        return TraceDriver(context = this, sink = sink)
    }
}

El inicializador automático detecta que tu subclase Application implementa Factory y usa tu fábrica de controladores personalizados.

JVM

En la JVM, no hay un mecanismo de bootstrapping automático. La aplicación es responsable de inicializar el TraceDriver y registrar el Tracer de forma global durante el inicio, por lo general, en tu función main.

Para registrar el tracer, llama a Tracer.setGlobalTracer().

import androidx.tracing.Tracer
import androidx.tracing.DelicateTracingApi
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File

fun main() {
    // Create the TraceSink, and the `TraceDriver`
    val outputDirectory = File("/tmp/perfetto")
    val sink = TraceSink(directory = outputDirectory)
    val driver = TraceDriver(sink = sink, isEnabled = true)

    // Register the tracer
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    // Call driver.close() as a result of the process shutdown hook.
    Runtime.getRuntime().addShutdownHook(Thread {
        driver.close()
    })
}

Uso básico

Un TraceSink define cómo se serializan los paquetes de seguimiento. Tracing 2.0.0 incluye una implementación de un receptor que usa el formato de paquete de seguimiento Perfetto. Un TraceDriver proporciona un controlador al Tracer y se puede usar para finalizar un seguimiento.

Una vez que se inicializa el Tracer, ya sea automáticamente en Android o de forma manual en la JVM, usa la instancia global Tracer.global para emitir eventos de seguimiento.

También puedes usar el TraceDriver para inhabilitar todos los puntos de seguimiento en la aplicación, si decides no realizar un seguimiento en algunas variantes de la aplicación. De manera opcional, puedes habilitar puntos de seguimiento para una category determinada si proporcionas una implementación para isCategoryEnabled cuando creas una instancia de TraceDriver.

val driver = TraceDriver(
    sink = sink,
    isCategoryEnabled = { category ->
        // Only enable trace points in the "com.example" package
        category.startsWith("com.example")
    }
)

A continuación, se muestra un ejemplo básico de cómo emitir un evento de seguimiento con Tracer.global en la JVM, incluida la configuración manual:

import androidx.tracing.Tracer
import androidx.tracing.DelicateTracingApi
import androidx.tracing.wire.TraceDriver
import androidx.tracing.wire.TraceSink
import java.io.File

// Category names should also follow the same convention used for package names
// on Android and Java. This makes them easier to identify and filter.
internal const val CATEGORY_MAIN = "com.example"

fun createSink(): TraceSink {
    val outputDirectory = File("/tmp/perfetto")
    if (!outputDirectory.exists()) {
        outputDirectory.mkdirs()
    }
    return TraceSink(directory = outputDirectory)
}

fun createTraceDriver(): TraceDriver {
    return TraceDriver(sink = createSink(), isCategoryEnabled = {true})
}

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(category = CATEGORY_MAIN, name = "basic") {
            // The block of code that needs to be traced.
            Thread.sleep(100L)
        }
    }
}

Esto genera el siguiente seguimiento.

Captura de pantalla de un registro básico de Perfetto

Figura 1: Captura de pantalla de un seguimiento básico de Perfetto

Puedes ver que se propagan las pistas de proceso y subproceso correctas, y que produjeron una sola sección de seguimiento basic, que se ejecutó durante 100 ms.

Las secciones de seguimiento (o segmentos) se pueden anidar en la misma pista para representar eventos superpuestos. A continuación, se muestra un ejemplo.

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(
            category = CATEGORY_MAIN,
            name = "processImage",
        ) {
            // Load the data first, then apply the sharpen filter
            sharpen(output = loadImage())
        }
    }
}

internal fun loadImage(): ByteArray {
    return Tracer.global.trace(CATEGORY_MAIN, "loadImage") {
        // Loads an image
        // ...
        // A placeholder
        ByteArray(0)
    }
}

internal fun sharpen(output: ByteArray) {
    // ...
    Tracer.global.trace(CATEGORY_MAIN, "sharpen") {
        // ...
    }
}

Esto genera el siguiente seguimiento.

Captura de pantalla de un registro básico de Perfetto con secciones anidadas

Figura 2: Captura de pantalla de un seguimiento básico de Perfetto con secciones anidadas

Puedes ver que hay eventos superpuestos en la pista del subproceso principal. Es muy claro que processImage llama a loadImage y sharpen en el mismo subproceso.

Agrega metadatos adicionales en las secciones de seguimiento

A veces, puede ser útil adjuntar metadatos contextuales adicionales a un segmento de seguimiento para obtener más detalles. Algunos ejemplos de estos metadatos podrían incluir el nav destination en el que se encuentra el usuario o los input arguments que podrían determinar cuánto tiempo tarda una función.

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(
            category = CATEGORY_MAIN,
            name = "basicWithContext",
            // Add additional metadata
            metadataBlock = {
                // Add key value pairs.
                addMetadataEntry("key", "value")
                addMetadataEntry("count", 1L)
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

Esto produce el siguiente resultado. Ten en cuenta que la sección Arguments contiene pares clave-valor agregados cuando se produce el slice.

Captura de pantalla de un registro básico de Perfetto con metadatos adicionales

Figura 3: Captura de pantalla de un seguimiento básico de Perfetto con metadatos adicionales

Propagación del contexto

Cuando se usan corrutinas de Kotlin o frameworks similares que ayudan con las cargas de trabajo simultáneas, Tracing 2.0 admite la noción de propagación del contexto. Esto se explica mejor con un ejemplo.

suspend fun taskOne() {
    Tracer.global.traceCoroutine(category = CATEGORY_MAIN, "taskOne") {
        delay(timeMillis = 100L)
    }
}

suspend fun taskTwo() {
    Tracer.global.traceCoroutine(category = CATEGORY_MAIN, "taskTwo") {
        delay(timeMillis = 50L)
    }
}

fun main() = runBlocking(context = Dispatchers.Default) {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
              taskOne()
              taskTwo()
            }
        }
        println("All done")
    }
}

Esto produce el siguiente resultado.

Captura de pantalla de un registro de Perfetto con propagación del contexto

Figura 4: Captura de pantalla de un seguimiento básico de Perfetto con propagación del contexto

La propagación del contexto facilita mucho la visualización del flujo de ejecución. Puedes ver exactamente qué tareas estaban relacionadas (conectadas a otras) y exactamente cuándo se suspendieron y reanudaron los Threads.

Por ejemplo, puedes ver que el segmento main generó taskOne y taskTwo. Después de eso, ambos subprocesos estuvieron inactivos porque las corrutinas se suspendieron debido al uso de delay.

Propagación manual

A veces, cuando combinas cargas de trabajo simultáneas con corrutinas de Kotlin con instancias de Executor de Java, puede ser útil propagar el contexto de una a otra. A continuación, se muestra un ejemplo:

fun executorTask(
    token: PropagationToken,
    executor: Executor,
    callback: () -> Unit
) {
    executor.execute {
        Tracer.global.trace(
            category = CATEGORY_MAIN,
            name = "executeTask",
            token = token,
        ) {
            // Do something
            Thread.sleep(100)
            callback()
        }
    }
}

fun main() = runBlocking(context = Dispatchers.Default) {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    val executor = Executors.newSingleThreadExecutor()
    driver.use {
        Tracer.global.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                val deferred = CompletableDeferred<Unit>()
                executorTask(
                    // Obtain the propagation token from the CoroutineContext
                    token = Tracer.global.tokenFromCoroutineContext(),
                    executor = executor,
                    callback = {
                        deferred.complete(Unit)
                    }
                )
                deferred.await()
            }
        }
        executor.shutdownNow()
    }
}

Esto produce el siguiente resultado.

Captura de pantalla de un registro de Perfetto con propagación manual del contexto

Figura 5: Captura de pantalla de un seguimiento básico de Perfetto con propagación manual del contexto

Puedes ver que la ejecución comenzó en un CoroutineContext y, luego, cambió a un Executor de Java, pero aún pudimos usar la propagación del contexto.

Combina con seguimientos del sistema

La biblioteca androidx.tracing no captura información como la programación de la CPU, el uso de memoria y la interacción de la aplicación con el sistema operativo en general. Esto se debe a que la biblioteca proporciona una forma de realizar un seguimiento en el proceso de baja sobrecarga.

Sin embargo, es muy sencillo combinar seguimientos del sistema con seguimientos en el proceso y visualizarlos como un solo seguimiento si es necesario. Esto se debe a que Perfetto UI admite la visualización de varios archivos de seguimiento de un dispositivo en una línea de tiempo unificada.

Para ello, puedes iniciar una sesión de seguimiento del sistema con Perfetto UI siguiendo las instrucciones que se indican aquí.

También puedes registrar eventos de seguimiento en el proceso con la API de Tracing 2.0 mientras está activado el seguimiento del sistema. Una vez que tengas ambos archivos de seguimiento, podrás usar la opción Open Multiple Trace Files en Perfetto.

Cómo abrir varios archivos de registro en la IU de Perfetto

Figura 6: Apertura de varios archivos de seguimiento en la IU de Perfetto

Flujos de trabajo avanzados

En esta sección, se describen los flujos de trabajo avanzados que puedes implementar con la biblioteca de seguimiento en el proceso.

Correlaciona segmentos

A veces, es útil atribuir segmentos en un seguimiento a una acción del usuario de nivel superior o a un evento del sistema. Por ejemplo, para atribuir todos los segmentos que corresponden a algún trabajo en segundo plano como parte de una notificación, puedes hacer lo siguiente:

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        onEvent(eventId = EVENT_ID)
    }
}

fun onEvent(eventId: Long) {
    Tracer.global.trace(
        category = CATEGORY_MAIN,
        name = "step-1",
        metadataBlock = {
            addCorrelationId(eventId)
        }
    ) {
        Thread.sleep(100L)
    }

    Thread.sleep(20)

    Tracer.global.trace(
        category = CATEGORY_MAIN,
        name = "step-2",
        metadataBlock = {
            addCorrelationId(eventId)
        }
    ) {
        Thread.sleep(180)
    }
}

Esto produce el siguiente resultado.

Captura de pantalla de un registro de Perfetto con segmentos correlacionados

Figura 7: Captura de pantalla de un seguimiento de Perfetto con segmentos correlacionados

Agrega información de la pila de llamadas

Las herramientas del host, como los complementos del compilador y los procesadores de anotaciones, también pueden optar por incorporar información de la pila de llamadas en un seguimiento para que sea conveniente ubicar el archivo, la clase o el método responsable de producir una sección de seguimiento en un seguimiento.

fun main() {
    val driver = createTraceDriver()
    @OptIn(DelicateTracingApi::class)
    Tracer.setGlobalTracer(driver.tracer)

    driver.use {
        Tracer.global.trace(
            category = CATEGORY_MAIN,
            name = "callStackEntry",
            metadataBlock = {
                addCallStackEntry(
                    name = "main",
                    lineNumber = 14,
                    sourceFile = "Basic.kt"
                )
            }
        ) {
            Thread.sleep(100L)
        }
    }
}

Esto produce el siguiente resultado.

Captura de pantalla de un registro de Perfetto con información de la pila de llamadas

Figura 8: Captura de pantalla de un seguimiento de Perfetto con información de la pila de llamadas