La biblioteca androidx.tracing:tracing:2.0.2 es una API de Kotlin de baja sobrecarga que te permite capturar eventos de registro en el proceso. Estos eventos pueden capturar períodos y su contexto. La biblioteca también admite la propagación del contexto para las corrutinas de Kotlin.
La biblioteca usa el mismo formato de paquete de registro de Perfetto que conocen los desarrolladores de Android. Además, Tracing 2.0 (a diferencia de las APIs de 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 registro, debes definir las dependencias en tu build.gradle.kts.
Proyectos de Kotlin Multiplatform
Las bibliotecas que solo necesitan emitir eventos de seguimiento deben depender de la API de androidx.tracing:tracing liviana. 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.2")
}
}
androidMain {
dependencies {
// Android implementation (includes the Perfetto Sink and automatic initialization)
implementation("androidx.tracing:tracing-wire:2.0.2")
}
}
jvmMain {
dependencies {
// JVM implementation
implementation("androidx.tracing:tracing-wire:2.0.2")
}
}
}
}
Proyectos solo para Android
Si solo segmentas tu aplicación para 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.2")
// For applications to configure the tracing backend
implementation("androidx.tracing:tracing-wire:2.0.2")
}
Inicialización y descubrimiento
Antes de poder 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 al inicio de la aplicación con la biblioteca androidx.startup.
De forma predeterminada, esta inicialización automática hace lo siguiente:
Crea un
TraceDrivercon unTraceSinkque escribe archivos de registro de Perfetto enContext.noBackupFilesDir/perfetto_traces/.Registra el
Tracerresultante de forma global.
Personaliza la instancia de TraceDriver
Si necesitas personalizar la configuración, por ejemplo, para cambiar la ubicación en la que se guardan los archivos de registro o usar un TraceSink personalizado, puedes proporcionar tu propia instancia de 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 personalizada.
JVM
En la JVM, no hay un mecanismo de arranque automático. La aplicación es responsable de inicializar TraceDriver y registrar Tracer de forma global durante el inicio, por lo general, en la función main.
Para registrar el objeto de seguimiento, 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 objeto TraceDriver proporciona un identificador para el objeto Tracer y se puede usar para finalizar un registro.
Una vez que se inicializa Tracer, ya sea automáticamente en Android o de forma manual en la JVM, usa la instancia global Tracer.global para emitir eventos de registro.
También puedes usar TraceDriver para inhabilitar todos los puntos de seguimiento en la aplicación si decides no realizar ningún seguimiento en algunas variantes de la aplicación. De manera opcional, puedes habilitar puntos de seguimiento para un category determinado 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 incluye un ejemplo básico de cómo emitir un evento de registro 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 registro.
Figura 1: Captura de pantalla de un registro básico de Perfetto.
Puedes ver que se propagaron los registros de proceso y subproceso correctos, y que produjeron una sola sección de registro basic, que se ejecutó durante 100 ms.
Las secciones (o segmentos) de registro se pueden anidar en el mismo segmento 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 registro.
Figura 2: Captura de pantalla de un registro básico de Perfetto con secciones anidadas.
Puedes ver que hay eventos superpuestos en el segmento 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 registro para obtener más detalles. Algunos ejemplos de estos metadatos podrían incluir el nav destination en el que se encuentra el usuario o el input arguments que podría determinar cuánto tiempo lleva 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.
Figura 3: Captura de pantalla de un registro 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 de 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.
Figura 4: Captura de pantalla de un registro básico de Perfetto con propagación de contexto.
La propagación del contexto facilita mucho la visualización del flujo de ejecución. Puedes ver exactamente qué tareas estaban relacionadas (conectadas con otras) y cuándo se suspendieron y reanudaron las Threads.
Por ejemplo, puedes ver que la segmentación main generó taskOne y taskTwo.
Después de eso, ambos subprocesos quedaron 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 y 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.
Figura 5: Captura de pantalla de un registro 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 registros del sistema
La biblioteca de 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 con una sobrecarga baja.
Sin embargo, es muy sencillo combinar los registros del sistema con los registros en proceso y visualizarlos como un solo registro si es necesario. Esto se debe a que Perfetto UI admite la visualización de varios archivos de registro de un dispositivo en una línea de tiempo unificada.
Para ello, puedes iniciar una sesión de registro del sistema con Perfetto UI siguiendo las instrucciones que se indican aquí.
También puedes registrar eventos de registro en proceso con la API de Tracing 2.0 mientras el registro del sistema está activado. Una vez que tengas ambos archivos de registro, puedes usar la opción Open Multiple Trace Files en Perfetto.
Figura 6: Apertura de varios archivos de registro 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 registro 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, podrías hacer algo como 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.
Figura 7: Captura de pantalla de un registro 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 registro para que sea más fácil ubicar el archivo, la clase o el método responsable de producir una sección de registro en un registro.
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.
Figura 8: Captura de pantalla de un registro de Perfetto con información de la pila de llamadas.