Rastreamento em processo (experimental)

A nova biblioteca androidx.tracing:tracing:2.0.0-alpha01 é uma API Kotlin de baixa sobrecarga que permite capturar eventos de rastreamento no processo. Esses eventos podem capturar intervalos de tempo e o contexto deles. A biblioteca também oferece suporte à propagação de contexto para corrotinas do Kotlin.

A biblioteca usa o mesmo formato de pacote de rastreamento Perfetto que os desenvolvedores do Android já conhecem. Além disso, o rastreamento 2.0 (ao contrário das APIs 1.0.0-*) oferece suporte à noção de backends de rastreamento conectáveis e coletores. Assim, outras bibliotecas de rastreamento podem personalizar o formato de rastreamento de saída e como a propagação de contexto funciona na implementação delas.

Dependências

Para começar o rastreamento, defina as seguintes dependências no build.gradle.kts.

kotlin {
  androidLibrary {
    namespace = "com.example.library"
    // ...
  }
  sourceSets {
    androidMain {
      dependencies {
        api("androidx.tracing:tracing-wire-android:2.0.0-alpha01")
        // ...
      }
    }
    jvmMain {
      dependencies {
        api("androidx.tracing:tracing-wire-desktop:2.0.0-alpha01")
        // ...
      }
    }
  }
}

Declare uma dependência em androidx.tracing:tracing-wire-android:2.0.0-alpha01 se você estiver segmentando uma biblioteca ou um aplicativo Android. Você pode usar a dependência androidx.tracing:tracing-wire-desktop:2.0.0-alpha01 se estiver segmentando a JVM.

Uso básico

Um TraceSink define como os pacotes de rastreamento são serializados. O Tracing 2.0.0 vem com uma implementação de um Sink que usa o formato de pacote de rastreamento Perfetto. Um TraceDriver fornece um identificador para o Tracer e pode ser usado para finalizar um rastreamento.

Você também pode usar o TraceDriver para desativar todos os pontos de rastreamento no aplicativo, caso não queira rastrear em algumas variantes do aplicativo. As futuras APIs no TraceDriver também vão permitir que os desenvolvedores controlem quais categorias de rastreamento eles querem capturar (ou desativar quando uma categoria for ruidosa).

Para começar, crie uma instância de TraceSink e 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.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
}

Depois de ter uma instância de TraceDriver, receba o Tracer, que define o ponto de entrada para todas as APIs de rastreamento.

// 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 {
        driver.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
            Thread.sleep(100L)
        }
    }
}

Isso gera o seguinte rastreamento.

Captura de tela de um rastreamento básico do Perfetto

Figura 1. Captura de tela de um rastreamento básico do Perfetto.

É possível ver que os rastreamentos de processo e de linhas de execução corretos foram preenchidos e produziram uma única seção de rastreamento basic, que foi executada por 100ms.

As seções (ou intervalos) de rastreamento podem ser aninhadas na mesma faixa para representar eventos sobrepostos. Confira um exemplo.

fun main() {
    // Initialize the tracing infrastructure to monitor app performance
    val driver = createTraceDriver()
    val tracer = driver.tracer
    driver.use {
        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") {
        // ...
    }
}

Isso gera o seguinte rastreamento.

Captura de tela de um rastreamento básico do Perfetto com seções aninhadas

Figura 2. Captura de tela de um rastreamento básico do Perfetto com seções aninhadas.

É possível ver que há eventos sobrepostos na faixa da linha de execução principal. É muito claro que processImage chama loadImage e sharpen na mesma linha de execução.

Adicionar mais metadados nas seções de rastreamento

Às vezes, pode ser útil anexar mais metadados contextuais a uma fração de rastreamento para ter mais detalhes. Alguns exemplos de metadados incluem o nav destination em que o usuário está ou input arguments que pode acabar determinando quanto tempo uma função leva.

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

Isso produz o seguinte resultado. A seção Arguments contém pares de chave-valor adicionados ao produzir o slice.

Captura de tela de um rastreamento básico do Perfetto com metadados adicionais

Figura 3. Captura de tela de um rastreamento básico do Perfetto com metadados adicionais.

Propagação de contexto

Ao usar corrotinas Kotlin (ou outros frameworks semelhantes que ajudam com cargas de trabalho simultâneas), o Tracing 2.0 oferece suporte à noção de propagação de contexto. Vamos explicar isso com um exemplo.

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 {
        tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
            coroutineScope {
                launch { taskOne(tracer) }
                launch { taskTwo(tracer) }
            }
        }
        println("All done")
    }
}

Isso produz o seguinte resultado.

Captura de tela de um rastreamento do Perfetto com propagação de contexto

Figura 4. Captura de tela de um rastreamento básico do Perfetto com propagação de contexto.

A propagação de contexto simplifica muito a visualização do fluxo de execução. Você pode ver exatamente quais tarefas estavam relacionadas (conectadas a outras) e quando Threads foram suspensas e retomadas.

Por exemplo, é possível ver que a fração main gerou taskOne e taskTwo. Depois disso, as duas linhas de execução ficaram inativas (já que as corrotinas foram suspensas devido ao uso de delay).

Propagação manual

Às vezes, ao misturar cargas de trabalho simultâneas usando corrotinas do Kotlin com instâncias de Executor do Java, pode ser útil propagar o contexto de uma para outra. Confira um exemplo:

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 {
        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()
    }
}

Isso produz o seguinte resultado.

Captura de tela de um rastreamento do Perfetto com propagação manual de contexto

Figura 5. Captura de tela de um rastreamento básico do Perfetto com propagação manual de contexto.

É possível ver que a execução começou em um CoroutineContext e, depois, mudou para um Executor Java, mas ainda foi possível usar a propagação de contexto.

Combinar com rastreamentos do sistema

O novo androidx.tracing não captura informações como programação da CPU, uso da memória e interação dos aplicativos com o sistema operacional em geral. Isso porque a biblioteca oferece uma maneira de realizar um rastreamento no processo de baixa sobrecarga.

No entanto, é muito fácil mesclar rastreamentos do sistema com rastreamentos no processo e visualizá-los como um único rastreamento, se necessário. Isso acontece porque o Perfetto UI permite visualizar vários arquivos de rastreamento de um dispositivo em uma linha do tempo unificada.

Para isso, inicie uma sessão de rastreamento do sistema usando Perfetto UI seguindo estas instruções.

Também é possível gravar eventos de rastreamento em processo usando a API Tracing 2.0 enquanto o rastreamento do sistema está ativado. Depois de ter os dois arquivos de rastreamento, use a opção Open Multiple Trace Files no Perfetto.

Abrir vários arquivos de rastreamento na interface do Perfetto

Figura 6. Abrir vários arquivos de rastreamento na interface do Perfetto.

Fluxos de trabalho avançados

Correlacionar intervalos

Às vezes, é útil atribuir partes de um rastreamento a uma ação do usuário de nível mais alto ou a um evento do sistema. Por exemplo, para atribuir todas as partes que correspondem a algum trabalho em segundo plano como parte de uma notificação, você pode fazer algo como:

fun main() {
    val driver = createTraceDriver()
    onEvent(driver, eventId = EVENT_ID)
}

fun onEvent(driver: TraceDriver, eventId: Long) {
    driver.use {
        driver.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)
        }
    }
}

Isso produz o seguinte resultado.

Captura de tela de um rastreamento do Perfetto com intervalos correlacionados

Figura 7. Captura de tela de um rastreamento do Perfetto com intervalos correlacionados.

Adicionar informações de call stack

As ferramentas do lado do host (plug-ins do compilador, processadores de anotações etc.) também podem incorporar informações de pilha de chamadas em um rastreamento para facilitar a localização do arquivo, da classe ou do método responsável por produzir uma seção de rastreamento em um rastreamento.

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

Isso produz o seguinte resultado.

Captura de tela de um rastreamento do Perfetto com informações da pilha de chamadas

Figura 8. Captura de tela de um rastreamento do Perfetto com informações da pilha de chamadas.