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
TraceDrivercon unTraceSinkche scrive i file di traccia Perfetto inContext.noBackupFilesDir/perfetto_traces/.Registra l'
Tracerrisultante 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.
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.
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.
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.
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.
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.
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.
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.
Immagine 8. Acquisizione dello schermo di una traccia Perfetto con informazioni sullo stack di chiamate.