Tracciamento in corso

La libreria androidx.tracing:tracing:2.0.0-beta01 è un'API Kotlin a basso overhead che consente di acquisire eventi di traccia in-process. Questi eventi possono acquisire intervalli di tempo e il relativo contesto. La libreria supporta anche la propagazione del contesto per le coroutine Kotlin.

La libreria utilizza lo stesso formato di pacchetto di traccia Perfetto che gli sviluppatori Android conoscono bene. Inoltre, Tracing 2.0 (a differenza delle API 1.0.0-*) supporta il concetto di backend di tracciamento plug-in e sink, in modo che altre librerie di tracciamento possano personalizzare il formato di tracciamento dell'output e il funzionamento della propagazione del contesto nella loro implementazione.

Dipendenze

Per iniziare la tracciabilità, devi definire le dipendenze in build.gradle.kts.

Progetti multipiattaforma Kotlin

Le librerie che devono solo emettere eventi di traccia devono dipendere dall'API androidx.tracing:tracing leggera. Le app che configurano il backend di tracciamento devono dipendere anche da 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")
      }
    }
  }
}

Progetti solo per Android

Se il targeting è solo per Android, aggiungi quanto segue al file build.gradle.kts dell'applicazione o della libreria:

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

Inizializzazione e rilevamento

Prima di poter registrare gli eventi di traccia, devi inizializzare l'infrastruttura di tracciamento. Ciò comporta la creazione di un AbstractTraceDriver e la registrazione del relativo Tracer a livello globale.

Android

Su Android, se includi la dipendenza androidx.tracing:tracing-wire, l'inizializzazione avviene automaticamente all'avvio dell'applicazione utilizzando la libreria androidx.startup.

Per impostazione predefinita, questa inizializzazione automatica esegue le seguenti operazioni:

  • Crea un TraceDriver con un TraceSink che scrive i file di traccia Perfetto in Context.noBackupFilesDir/perfetto_traces/.

  • Registra l'Tracer risultante a livello globale.

Personalizza l'istanza TraceDriver

Se devi personalizzare la configurazione, ad esempio per modificare la posizione in cui vengono salvati i file di traccia o per utilizzare un TraceSink personalizzato, puoi fornire la tua istanza AbstractTraceDriver.

Per personalizzare la configurazione, fai in modo che la tua classe Application implementi 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)
    }
}

L'inizializzatore automatico rileva che la sottoclasse Application implementa Factory e utilizza la tua factory di driver personalizzata.

JVM

Nella JVM non esiste un meccanismo di bootstrapping automatico. L'applicazione è responsabile dell'inizializzazione di TraceDriver e della registrazione di Tracer a livello globale durante l'avvio, in genere nella funzione main.

Per registrare il tracker, chiama il numero 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()
    })
}

Utilizzo di base

Un TraceSink definisce la modalità di serializzazione dei pacchetti di traccia. La versione 2.0.0 di Tracing include un'implementazione di un Sink che utilizza il formato del pacchetto di traccia Perfetto. Un TraceDriver fornisce un handle per Tracer e può essere utilizzato per finalizzare una traccia.

Una volta inizializzato Tracer, automaticamente su Android o manualmente sulla JVM, utilizza l'istanza globale Tracer.global per emettere eventi di traccia.

Puoi anche utilizzare TraceDriver per disattivare tutti i punti di traccia nell'applicazione, se scegli di non eseguire il tracciamento in alcune varianti dell'applicazione. Se vuoi, puoi attivare i punti di traccia per un determinato category fornendo un'implementazione per isCategoryEnabled quando crei un'istanza di TraceDriver.

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

Ecco un esempio di base di emissione di un evento di tracciamento utilizzando Tracer.global sulla JVM, inclusa la configurazione manuale:

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

Viene generata la seguente traccia.

Screenshot di una traccia Perfetto di base

Figura 1. Acquisizione schermata di una traccia Perfetto di base.

Puoi vedere che le tracce di processo e thread corrette sono compilate e che hanno prodotto una singola sezione di traccia basic, che è stata eseguita per 100 ms.

Le sezioni (o i segmenti) della traccia possono essere nidificate sulla stessa traccia per rappresentare eventi sovrapposti. Ecco un esempio.

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

Viene generata la seguente traccia.

Acquisizione dello schermo di una traccia Perfetto di base con sezioni nidificate

Figura 2. Acquisizione schermo di una traccia Perfetto di base con sezioni nidificate.

Puoi notare che ci sono eventi sovrapposti nella traccia del thread principale. È molto chiaro che le chiamate processImage e loadImage e sharpen si trovano nello stesso thread.

Aggiungere metadati aggiuntivi nelle sezioni della traccia

A volte, può essere utile allegare metadati contestuali aggiuntivi a una sezione di traccia per ottenere maggiori dettagli. Alcuni esempi di questi metadati potrebbero includere il nav destination su cui si trova l'utente o input arguments che potrebbe finire per determinare la durata di una funzione.

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

Questo produce il seguente risultato. Tieni presente che la sezione Arguments contiene coppie chiave-valore aggiunte durante la produzione di slice.

Acquisizione schermo di una traccia Perfetto di base con metadati aggiuntivi

Figura 3. Acquisizione dello schermo di una traccia Perfetto di base con metadati aggiuntivi.

Propagazione del contesto

Quando utilizzi le coroutine Kotlin o altri framework simili che aiutano con i workload simultanei, Tracing 2.0 supporta il concetto di propagazione del contesto. Il modo migliore per spiegarlo è con un esempio.

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

Questo produce il seguente risultato.

Acquisizione dello schermo di una traccia Perfetto con propagazione del contesto

Figura 4. Acquisizione schermo di una traccia Perfetto di base con propagazione del contesto.

La propagazione del contesto semplifica notevolmente la visualizzazione del flusso di esecuzione. Puoi vedere esattamente quali attività erano correlate (collegate ad altre) e quando esattamente Threads sono state sospese e ripristinate.

Ad esempio, puoi vedere che la sezione main ha generato taskOne e taskTwo. Dopodiché, entrambi i thread sono rimasti inattivi perché le coroutine sono state sospese a causa dell'utilizzo di delay.

Propagazione manuale

A volte, quando combini carichi di lavoro simultanei utilizzando le coroutine Kotlin con istanze di Executor Java, potrebbe essere utile propagare il contesto da uno all'altro. Ecco un esempio:

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

Questo produce il seguente risultato.

Acquisizione dello schermo di una traccia Perfetto con propagazione manuale del contesto

Figura 5. Acquisizione schermo di una traccia Perfetto di base con propagazione manuale del contesto.

Puoi vedere che l'esecuzione è iniziata in un CoroutineContext e successivamente è passata a un Executor Java, ma siamo comunque riusciti a utilizzare la propagazione del contesto.

Combinare con le tracce di sistema

La libreria androidx.tracing non acquisisce informazioni come la pianificazione della CPU, l'utilizzo della memoria e l'interazione dell'applicazione con il sistema operativo in generale. Questo perché la libreria fornisce un modo per eseguire la tracciabilità in-process a basso overhead.

Tuttavia, è estremamente semplice unire le tracce di sistema con le tracce in-process e visualizzarle come una singola traccia, se necessario. Questo perché Perfetto UI supporta la visualizzazione di più file di traccia di un dispositivo in una cronologia unificata.

Per farlo, puoi avviare una sessione di tracciamento del sistema utilizzando Perfetto UI seguendo le istruzioni riportate qui.

Puoi anche registrare gli eventi di traccia in-process utilizzando l'API Tracing 2.0, mentre la traccia di sistema è attiva. Una volta ottenuti entrambi i file di traccia, puoi utilizzare l'opzione Open Multiple Trace Files in Perfetto.

Apertura di più file di traccia nella UI di Perfetto

Figura 6. Apertura di più file di traccia in Perfetto UI.

Flussi di lavoro avanzati

Questa sezione descrive i flussi di lavoro avanzati che puoi implementare con la libreria di tracciamento in-process.

Correlare le sezioni

A volte è utile attribuire le sezioni di una traccia a un'azione utente di livello superiore o a un evento di sistema. Ad esempio, per attribuire tutte le sezioni che corrispondono a un lavoro in background nell'ambito di una notifica, potresti fare qualcosa del genere:

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

Questo produce il seguente risultato.

Acquisizione dello schermo di una traccia Perfetto con sezioni correlate

Figura 7. Acquisizione schermo di una traccia Perfetto con sezioni correlate.

Aggiungere informazioni sullo stack di chiamate

Gli strumenti lato host, come i plug-in del compilatore e i processori di annotazioni, possono anche scegliere di incorporare le informazioni sullo stack di chiamate in una traccia, per facilitare l'individuazione del file, della classe o del metodo responsabile della produzione di una sezione di traccia in una traccia.

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

Questo produce il seguente risultato.

Acquisizione schermo di una traccia Perfetto con informazioni sullo stack di chiamate

Immagine 8. Acquisizione dello schermo di una traccia Perfetto con informazioni sullo stack di chiamate.