Die Bibliothek androidx.tracing:tracing:2.0.2 ist eine Kotlin-API mit geringem Overhead, mit der Sie In-Process-Trace-Ereignisse erfassen können. Mit diesen Ereignissen können Zeitabschnitte und ihr Kontext erfasst werden. Die Bibliothek unterstützt auch die Kontextweitergabe für Kotlin-Coroutinen.
Die Bibliothek verwendet dasselbe Perfetto-Trace-Paketformat, das Android-Entwickler kennen. Außerdem unterstützt Tracing 2.0 (im Gegensatz zu den 1.0.0-*-APIs) das Konzept von einsteckbaren Tracing-Back-Ends und Senken. Andere Tracing-Bibliotheken können das Ausgabetracing-Format und die Funktionsweise der Kontextweitergabe in ihrer Implementierung anpassen.
Abhängigkeiten
Um mit dem Tracing zu beginnen, müssen Sie die Abhängigkeiten in Ihrer build.gradle.kts definieren.
Kotlin Multiplatform-Projekte
Bibliotheken, die nur Trace-Ereignisse ausgeben müssen, sollten von der einfachen androidx.tracing:tracing API abhängen. Apps, die das Tracing-Backend konfigurieren, sollten auch von androidx.tracing:tracing-wire abhängen.
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")
}
}
}
}
Nur für Android-Projekte
Wenn Sie nur Android als Zielplattform haben, fügen Sie der Datei build.gradle.kts Ihrer Anwendung oder Bibliothek Folgendes hinzu:
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")
}
Initialisierung und Erkennung
Bevor Sie Trace-Ereignisse aufzeichnen können, müssen Sie die Tracing-Infrastruktur initialisieren. Dazu müssen Sie ein AbstractTraceDriver erstellen und dessen Tracer global registrieren.
Android
Wenn Sie unter Android die androidx.tracing:tracing-wire-Abhängigkeit einfügen, erfolgt die Initialisierung beim Start der Anwendung automatisch über die androidx.startup-Bibliothek.
Standardmäßig wird bei dieser automatischen Initialisierung Folgendes ausgeführt:
Erstellt eine
TraceDrivermit einemTraceSink, der Perfetto-Tracedateien inContext.noBackupFilesDir/perfetto_traces/schreibt.Registriert die resultierenden
Tracerglobal.
TraceDriver-Instanz anpassen
Wenn Sie die Konfiguration anpassen müssen, z. B. um den Speicherort für Tracedateien zu ändern oder eine benutzerdefinierte TraceSink zu verwenden, können Sie eine eigene AbstractTraceDriver-Instanz bereitstellen.
Wenn Sie die Konfiguration anpassen möchten, muss Ihre Application-Klasse AbstractTraceDriver.Factory implementieren:
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)
}
}
Der automatische Initialisierer erkennt, dass Ihre Application-Unterklasse Factory implementiert, und verwendet Ihre benutzerdefinierte Treiberfactory.
JVM
Auf der JVM gibt es keinen automatischen Bootstrapping-Mechanismus. Die Anwendung ist dafür verantwortlich, TraceDriver zu initialisieren und Tracer beim Start global zu registrieren, in der Regel in der Funktion main.
Rufen Sie Tracer.setGlobalTracer() auf, um den Tracer zu registrieren.
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()
})
}
Grundlegende Nutzung
Eine TraceSink definiert, wie Trace-Pakete serialisiert werden. Tracing 2.0.0 enthält eine Implementierung eines Sinks, der das Perfetto-Trace-Paketformat verwendet. Ein TraceDriver bietet ein Handle für den Tracer und kann verwendet werden, um einen Trace abzuschließen.
Sobald Tracer initialisiert ist (automatisch unter Android oder manuell auf der JVM), verwenden Sie die globale Tracer.global-Instanz, um Trace-Ereignisse auszugeben.
Sie können auch TraceDriver verwenden, um alle Trace-Punkte in der Anwendung zu deaktivieren, wenn Sie in einigen Anwendungs-Varianten keine Trace-Punkte verwenden möchten. Sie können optional Trace-Punkte für ein bestimmtes category aktivieren, indem Sie beim Erstellen einer Instanz von TraceDriver eine Implementierung für isCategoryEnabled bereitstellen.
val driver = TraceDriver(
sink = sink,
isCategoryEnabled = { category ->
// Only enable trace points in the "com.example" package
category.startsWith("com.example")
}
)
Hier ist ein einfaches Beispiel für das Ausgeben eines Trace-Ereignisses mit Tracer.global in der JVM, einschließlich der manuellen Einrichtung:
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)
}
}
}
Dadurch wird der folgende Trace generiert.
Abbildung 1. Screenshot eines einfachen Perfetto-Traces.
Sie sehen, dass die richtigen Prozess- und Thread-Tracks ausgefüllt wurden und dass sie einen einzelnen Trace-Abschnitt basic ergeben haben, der 100 ms lang ausgeführt wurde.
Trace-Abschnitte (oder ‑Slices) können auf demselben Track verschachtelt werden, um sich überschneidende Ereignisse darzustellen. Hier ein Beispiel:
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") {
// ...
}
}
Dadurch wird der folgende Trace generiert.
Abbildung 2: Screenshot eines einfachen Perfetto-Traces mit verschachtelten Abschnitten.
Sie sehen, dass sich Ereignisse im Hauptthread-Track überschneiden. Es ist ganz klar, dass processImage die Funktionen loadImage und sharpen im selben Thread aufruft.
Zusätzliche Metadaten in Trace-Abschnitten hinzufügen
Manchmal kann es nützlich sein, einem Trace-Segment zusätzliche kontextbezogene Metadaten anzuhängen, um weitere Details zu erhalten. Beispiele für solche Metadaten sind die nav destination des Nutzers oder input arguments, die letztendlich die Ausführungsdauer einer Funktion bestimmen können.
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)
}
}
}
Das führt zu folgendem Ergebnis. Der Abschnitt Arguments enthält Schlüssel/Wert-Paare, die beim Erstellen von slice hinzugefügt wurden.
Abbildung 3: Screenshot eines einfachen Perfetto-Traces mit zusätzlichen Metadaten.
Kontextweitergabe
Wenn Sie Kotlin-Coroutinen oder ähnliche Frameworks verwenden, die bei gleichzeitigen Arbeitslasten helfen, unterstützt Tracing 2.0 das Konzept der Kontextweitergabe. Das lässt sich am besten anhand eines Beispiels erklären.
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")
}
}
Das führt zu folgendem Ergebnis.
Abbildung 4: Screenshot eines einfachen Perfetto-Traces mit Kontextweitergabe.
Mit der Kontextübertragung lässt sich der Ablauf der Ausführung viel einfacher visualisieren. Sie können genau sehen, welche Aufgaben miteinander verknüpft waren und wann Threads angehalten und fortgesetzt wurden.
Sie können beispielsweise sehen, dass für das Segment main die Segmente taskOne und taskTwo generiert wurden.
Danach waren beide Threads inaktiv, da die Coroutinen aufgrund der Verwendung von delay angehalten wurden.
Manuelle Weitergabe
Wenn Sie gleichzeitige Arbeitslasten mit Kotlin-Coroutinen und Instanzen von Java Executor kombinieren, kann es manchmal sinnvoll sein, den Kontext von einer zur anderen zu übertragen. Hier ein Beispiel:
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()
}
}
Das führt zu folgendem Ergebnis.
Abbildung 5: Screenshot eines einfachen Perfetto-Traces mit manueller Kontextweitergabe.
Die Ausführung wurde in einem CoroutineContext gestartet und dann zu einem Java-Executor gewechselt. Die Kontextweitergabe war aber weiterhin möglich.
Mit System-Traces kombinieren
Die androidx.tracing-Bibliothek erfasst keine Informationen wie CPU-Scheduling, Arbeitsspeichernutzung und die Interaktion der Anwendung mit dem Betriebssystem im Allgemeinen. Das liegt daran, dass die Bibliothek eine Möglichkeit bietet, In-Process-Tracing mit geringem Overhead durchzuführen.
Es ist jedoch sehr einfach, System-Traces mit In-Process-Traces zusammenzuführen und sie bei Bedarf als einzelnen Trace zu visualisieren. Das liegt daran, dass Perfetto UI die Visualisierung mehrerer Tracedateien von einem Gerät auf einer einheitlichen Zeitachse unterstützt.
Dazu können Sie eine System-Tracing-Sitzung mit Perfetto UI starten. Folgen Sie dazu dieser Anleitung.
Sie können auch In-Process-Trace-Ereignisse mit der Tracing 2.0 API aufzeichnen, während das System-Tracing aktiviert ist. Sobald Sie beide Trace-Dateien haben, können Sie die Option Open Multiple Trace Files in Perfetto verwenden.
Abbildung 6. Mehrere Tracedateien in der Perfetto-UI öffnen
Erweiterte Workflows
In diesem Abschnitt werden erweiterte Workflows beschrieben, die Sie mit der In-Process-Tracing-Bibliothek implementieren können.
Segmente korrelieren
Manchmal ist es nützlich, Abschnitte in einem Trace einer Nutzeraktion oder einem Systemereignis auf höherer Ebene zuzuordnen. Wenn Sie beispielsweise alle Slices, die einer Hintergrundaufgabe als Teil einer Benachrichtigung entsprechen, zuordnen möchten, können Sie Folgendes tun:
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)
}
}
Das führt zu folgendem Ergebnis.
Abbildung 7. Screenshot eines Perfetto-Traces mit korrelierten Slices.
Aufrufstack-Informationen hinzufügen
Hostseitige Tools wie Compiler-Plug-ins und Annotationsprozessoren können auch Aufrufstack-Informationen in einen Trace einbetten, um die Datei, Klasse oder Methode, die für die Erstellung eines Trace-Abschnitts in einem Trace verantwortlich ist, leichter zu finden.
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)
}
}
}
Das führt zu folgendem Ergebnis.
Abbildung 8. Bildschirmaufnahme eines Perfetto-Traces mit Informationen zum Aufrufstack.