Rastreamento no processo

A biblioteca androidx.tracing:tracing:2.0.2 é 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 back-ends 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 a rastrear, defina as dependências no seu build.gradle.kts.

Projetos Kotlin Multiplatform

As bibliotecas que só precisam emitir eventos de rastreamento devem depender da API androidx.tracing:tracing leve. Os apps que configuram o back-end de rastreamento também precisam 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")
      }
    }
  }
}

Projetos somente para Android

Se você estiver segmentando apenas o Android, adicione o seguinte ao arquivo build.gradle.kts do aplicativo ou da 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")
}

Inicialização e descoberta

Antes de gravar eventos de rastreamento, é necessário inicializar a infraestrutura de rastreamento. Isso envolve a criação de um AbstractTraceDriver e o registro do Tracer dele globalmente.

Android

No Android, se você incluir a dependência androidx.tracing:tracing-wire, a inicialização vai acontecer automaticamente na inicialização do aplicativo usando a biblioteca androidx.startup.

Por padrão, essa inicialização automática faz o seguinte:

  • Cria um TraceDriver com um TraceSink que grava arquivos de rastreamento do Perfetto em Context.noBackupFilesDir/perfetto_traces/.

  • Registra o Tracer resultante globalmente.

Personalizar a instância TraceDriver

Se você precisar personalizar a configuração, por exemplo, para mudar onde os arquivos de rastreamento são salvos ou usar um TraceSink personalizado, forneça sua própria instância AbstractTraceDriver.

Para personalizar a configuração, faça com que sua classe 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)
    }
}

O inicializador automático detecta que sua subclasse Application implementa Factory e usa sua fábrica de drivers personalizada.

JVM

Na JVM, não há um mecanismo de inicialização automática. O aplicativo é responsável por inicializar o TraceDriver e registrar o Tracer globalmente durante a inicialização, geralmente na função main.

Para registrar o rastreador, chame 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

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.

Depois que o Tracer for inicializado, automaticamente no Android ou manualmente na JVM, use a instância global Tracer.global para emitir eventos de 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. Você pode ativar pontos de rastreamento para um determinado category fornecendo uma implementação para isCategoryEnabled ao criar uma instância de TraceDriver.

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

Confira um exemplo básico de emissão de um evento de rastreamento usando Tracer.global na JVM, incluindo a configuração 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)
        }
    }
}

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.

Você pode ver que os rastreamentos de processo e de linha de execução corretos são preenchidos e que eles produziram uma única seção de rastreamento basic, que foi executada por 100 ms.

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

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") {
        // ...
    }
}

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 em seções de rastreamento

Às vezes, é ú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()
    @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)
        }
    }
}

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.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")
    }
}

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, ambas as linhas de execução ficaram inativas porque 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 um para o outro. Confira um exemplo:

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

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 notar 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

A biblioteca androidx.tracing não captura informações como o agendamento da CPU, o uso da memória e a interação do aplicativo com o sistema operacional em geral. Isso porque a biblioteca oferece uma maneira de realizar rastreamento de baixo custo no processo.

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 fazer 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

Esta seção descreve fluxos de trabalho avançados que podem ser implementados com a biblioteca de rastreamento no processo.

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 fatias que correspondem a algum trabalho em segundo plano como parte de uma notificação, você pode fazer algo como:

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

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, como plug-ins de compilador e processadores de anotações, 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()
    @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)
        }
    }
}

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.