La nueva biblioteca androidx.tracing:tracing:2.0.0-alpha04 es una API de Kotlin de baja sobrecarga
que permite capturar eventos de seguimiento en el proceso. Estos eventos pueden capturar segmentos de tiempo y su contexto. Además, la biblioteca admite la propagación del contexto para las corrutinas de Kotlin.
La biblioteca usa el mismo Perfetto formato de paquete de seguimiento con el que están familiarizados 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 siguientes dependencias en build.gradle.kts.
kotlin {
androidLibrary {
namespace = "com.example.library"
// ...
}
sourceSets {
androidMain {
dependencies {
api("androidx.tracing:tracing-wire:2.0.0-alpha04")
// ...
}
}
jvmMain {
dependencies {
api("androidx.tracing:tracing-wire:2.0.0-alpha04")
// ...
}
}
}
}
Declara una dependencia en androidx.tracing:tracing-wire:2.0.0-alpha04 si te orientas a una biblioteca de Android, una aplicación para Android o la JVM.
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 para el Tracer y se puede usar para finalizar un seguimiento.
También puedes usar el TraceDriver para inhabilitar todos los puntos de seguimiento de la aplicación si decides no realizar el seguimiento en absoluto en algunas variantes de la aplicación.
Las futuras APIs en el TraceDriver también permitirán que los desarrolladores controlen qué categorías de seguimiento les interesa capturar (o inhabilitar cuando una categoría es ruidosa).
Para comenzar, crea una instancia de un TraceSink y un TraceDriver.
/**
* A [TraceSink] defines how traces are serialized.
*
* [androidx.tracing.wire.TraceSink] uses the `Perfetto` trace packet format.
*/
fun createSink(): TraceSink {
val outputDirectory = File(/* path = */ "/tmp/perfetto")
if (!outputDirectory.exists()) {
outputDirectory.mkdirs()
}
// We are using the factory function defined in androidx.tracing.wire
return TraceSink(
sequenceId = 1,
directory = outputDirectory
)
}
/**
* Creates a new instance of [androidx.tracing.wire.TraceDriver].
*/
fun createTraceDriver(): TraceDriver {
// We are using a factory function from androidx.tracing.wire here.
// `isEnabled` controls whether tracing is enabled for the application.
val driver = TraceDriver(sink = createSink(), isEnabled = true)
return driver
}
Después de tener una instancia de TraceDriver, obtén el Tracer que define el punto de entrada para todas las APIs de seguimiento.
// Tracing Categories identify subsystems that are responsible
// in generating trace sections. Future APIs in `TraceDriver` will allow the
// application to specify which categories they are interested in tracing.
// This lets the application disable entire trace categories, without
// needing to disable trace instrumentation at the call sites for those
// categories.
internal const val CATEGORY_MAIN = "main"
fun main() {
val driver = createTraceDriver()
driver.use {
it.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
Thread.sleep(100L)
}
}
}
Esto genera el siguiente seguimiento.
Figura 1: Captura de pantalla de un seguimiento básico de Perfetto
Puedes ver que se propagan las pistas correctas de proceso y subproceso, y se produce una sola sección de seguimiento basic, que se ejecutó durante 100ms.
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() {
// Initialize the tracing infrastructure to monitor app performance
val driver = createTraceDriver()
val tracer = driver.tracer
driver.use {
it.tracer.trace(
category = CATEGORY_MAIN,
name = "processImage",
) {
// Load the data first, then apply the sharpen filter
sharpen(tracer = tracer, output = loadImage(tracer))
}
}
}
internal fun loadImage(tracer: Tracer): ByteArray {
return tracer.trace(CATEGORY_MAIN, "loadImage") {
// Loads an image
// ...
// A placeholder
ByteArray(0)
}
}
internal fun sharpen(tracer: Tracer, output: ByteArray) {
// ...
tracer.trace(CATEGORY_MAIN, "sharpen") {
// ...
}
}
Esto genera el siguiente seguimiento.
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()
driver.use {
it.tracer.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 seguimiento básico de Perfetto con metadatos adicionales
Propagación del contexto
Cuando se usan corrutinas de Kotlin (o cualquier otro framework similar que ayude con las cargas de trabajo simultáneas), Tracing 2.0 admite la noción de propagación del contexto. La mejor manera de explicar esto es con un ejemplo.
suspend fun taskOne(tracer: Tracer) {
tracer.traceCoroutine(category = CATEGORY_MAIN, "taskOne") {
delay(timeMillis = 100L)
}
}
suspend fun taskTwo(tracer: Tracer) {
tracer.traceCoroutine(category = CATEGORY_MAIN, "taskTwo") {
delay(timeMillis = 50L)
}
}
fun main() = runBlocking(context = Dispatchers.Default) {
val driver = createTraceDriver()
val tracer = driver.tracer
driver.use {
it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
coroutineScope {
launch { taskOne(tracer) }
launch { taskTwo(tracer) }
}
}
println("All done")
}
}
Esto produce el siguiente resultado.
Figura 4: Captura de pantalla de un seguimiento básico de Perfetto con propagación del contexto
La propagación del contexto hace que sea mucho más fácil visualizar el 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 (dado que 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(
tracer: Tracer,
token: PropagationToken,
executor: Executor,
callback: () -> Unit
) {
executor.execute {
tracer.trace(
category = CATEGORY_MAIN,
name = "executeTask",
token = token,
) {
// Do something
Thread.sleep(100)
callback()
}
}
}
@OptIn(DelicateTracingApi::class)
fun main() = runBlocking(context = Dispatchers.Default) {
val driver = createTraceDriver()
val executor = Executors.newSingleThreadExecutor()
val tracer = driver.tracer
driver.use {
it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
coroutineScope {
val deferred = CompletableDeferred<Unit>()
executorTask(
tracer = tracer,
// Obtain the propagation token from the CoroutineContext
token = tracer.tokenFromCoroutineContext(),
executor = executor,
callback = {
deferred.complete(Unit)
}
)
deferred.await()
}
}
executor.shutdownNow()
}
}
Esto produce el siguiente resultado.
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
El nuevo androidx.tracing no captura información como la programación de la CPU, el uso de memoria y la interacción de las aplicaciones con el sistema operativo en general. Esto se debe a que la biblioteca proporciona una forma de realizar un seguimiento en el proceso de muy baja sobrecarga.
Sin embargo, es extremadamente trivial 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 el seguimiento del sistema está activado. Una vez que tengas ambos archivos de seguimiento, podrás usar la opción Open Multiple Trace Files en Perfetto.
Figura 6: Apertura de varios archivos de seguimiento en la IU de Perfetto
Flujos de trabajo avanzados
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()
onEvent(driver, eventId = EVENT_ID)
}
fun onEvent(driver: TraceDriver, eventId: Long) {
driver.use {
it.tracer.trace(
category = CATEGORY_MAIN,
name = "step-1",
metadataBlock = {
addCorrelationId(eventId)
}
) {
Thread.sleep(100L)
}
Thread.sleep(20)
driver.tracer.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 seguimiento de Perfetto con segmentos correlacionados
Agrega información de la pila de llamadas
Las herramientas del host (plugins del compilador, procesadores de anotaciones, etc.) 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()
driver.use {
it.tracer.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 seguimiento de Perfetto con información de la pila de llamadas