A biblioteca androidx.tracing:tracing:2.0.0-beta01 é uma API Kotlin de baixa sobrecarga
que permite capturar eventos de rastreamento no processo. Esses eventos podem capturar fatias de tempo e o contexto delas. A biblioteca também oferece suporte à propagação de contexto para corrotinas do Kotlin.
A biblioteca usa o mesmo Perfetto formato de pacote de rastreamento que os desenvolvedores Android
conhecem. Além disso, o Tracing 2.0 (ao contrário das APIs 1.0.0-*)
oferece suporte à noção de back-ends de rastreamento conectáveis e coletores, para que outras
bibliotecas de rastreamento possam personalizar o formato de rastreamento de saída e como a propagação de contexto
funciona na implementação delas.
Dependências
Para iniciar o rastreamento, defina as dependências no build.gradle.kts.
Projetos Kotlin Multiplatform
As bibliotecas que só precisam emitir eventos de rastreamento dependem da API leve androidx.tracing:tracing. Os apps que configuram o back-end de rastreamento também dependem 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")
}
}
}
}
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.0-beta01")
// For applications to configure the tracing backend
implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}
Inicialização e descoberta
Antes de gravar eventos de rastreamento, inicialize a infraestrutura de rastreamento. Isso envolve a criação de um AbstractTraceDriver e o registro global do Tracer.
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
TraceDrivercom umTraceSinkque grava arquivos de rastreamento do Perfetto emContext.noBackupFilesDir/perfetto_traces/.Registra o
Tracerresultante globalmente.
Personalizar a instância TraceDriver
Se você precisar personalizar a configuração, por exemplo, para mudar o local em que os arquivos de rastreamento são salvos ou para usar um TraceSink personalizado, forneça sua própria instância AbstractTraceDriver.
Para personalizar a configuração, faça com que a 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 driver 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, mais comumente 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 coletor 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 no
a JVM, use a instância global Tracer.global para emitir eventos de rastreamento.
Também é possível usar o TraceDriver para desativar todos os pontos de rastreamento no aplicativo, caso você não queira rastrear em algumas variantes do aplicativo. Opcionalmente, é possível ativar pontos de rastreamento para uma determinada 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.
Figura 1. Captura de tela de um rastreamento básico do Perfetto.
É possível observar que as faixas de processo e de linha de execução corretas são preenchidas e que elas produziram uma única seção de rastreamento basic, que foi executada por 100 ms.
As seções de rastreamento (ou fatias) 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.
Figura 2. Captura de tela de um rastreamento básico do Perfetto com seções aninhadas.
É possível observar que há eventos sobrepostos na faixa principal da linha de execução. É muito claro que processImage chama loadImage e sharpen na mesma linha de execução.
Adicionar mais metadados em seções de rastreamento
Às vezes, pode ser útil anexar mais metadados contextuais a uma fatia de rastreamento para receber mais detalhes. Alguns exemplos desses metadados podem incluir o nav destination em que o usuário está ou input arguments que podem 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. Observe que a seção Arguments contém pares de chave-valor adicionados ao produzir a slice.
Figura 3. Captura de tela de um rastreamento básico do Perfetto com mais metadados.
Propagação de contexto
Ao usar corrotinas do Kotlin ou outras estruturas semelhantes que ajudam com cargas de trabalho simultâneas, o Tracing 2.0 oferece suporte à noção de propagação de contexto. Isso é melhor explicado por 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.
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. É possível ver exatamente quais tarefas estavam relacionadas (conectadas a outras) e exatamente quando as Threads foram suspensas e retomadas.
Por exemplo, é possível observar que a fatia 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.
Figura 5. Captura de tela de um rastreamento básico do Perfetto com propagação de contexto manual.
É possível observar que a execução começou em um CoroutineContext e, posteriormente, mudou para um Executor do 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 agendamento da CPU, uso de memória e a interação do aplicativo com o sistema operacional em geral. Isso ocorre porque a biblioteca oferece uma maneira de realizar rastreamento no processo de baixa sobrecarga.
No entanto, é muito simples mesclar rastreamentos do sistema com rastreamentos no processo e visualizá-los como um único rastreamento, se necessário. Isso ocorre porque a Perfetto UI oferece suporte à visualização de 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 instruções aqui.
Também é possível gravar eventos de rastreamento no processo usando a API Tracing 2.0, enquanto o rastreamento do sistema está ativado. Depois de ter ambos os arquivos de rastreamento, use a opção Open Multiple Trace Files no Perfetto.
Figura 6. Como 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 fatias
Às vezes, é útil atribuir fatias em 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.
Figura 7. Captura de tela de um rastreamento do Perfetto com fatias correlacionadas.
Adicionar informações da pilha de chamadas
As ferramentas do lado do host, como plug-ins do compilador e processadores de anotação, também podem incorporar informações da 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.
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.
Figura 8. Captura de tela de um rastreamento do Perfetto com informações da pilha de chamadas.