In-Process-Tracing

Die androidx.tracing:tracing:2.0.0-beta01 Bibliothek ist eine ressourcenschonende Kotlin API, mit der Sie Trace-Ereignisse im Prozess 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 -Entwicklern bekannt ist. Außerdem unterstützt Tracing 2.0 (im Gegensatz zu den 1.0.0-* APIs) das Konzept von austauschbaren Tracing-Back-Ends und Senken, sodass andere Tracing-Bibliotheken das Ausgabe-Tracing-Format und die Funktionsweise der Kontext weitergabe in ihrer Implementierung anpassen können.

Abhängigkeiten

Um das Tracing zu starten, 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 ressourcenschonenden androidx.tracing:tracing API abhängen. Apps, die das Tracing-Back-End konfigurieren, sollten auch von androidx.tracing:tracing-wire abhängen.

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

Projekte, die nur auf Android ausgerichtet sind

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.0-beta01")

    // For applications to configure the tracing backend
    implementation("androidx.tracing:tracing-wire:2.0.0-beta01")
}

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 seinen Tracer global registrieren.

Android

Wenn Sie unter Android die Abhängigkeit androidx.tracing:tracing-wire einbeziehen, erfolgt die Initialisierung beim Start der Anwendung automatisch mit der Bibliothek androidx.startup.

Standardmäßig führt diese automatische Initialisierung Folgendes aus:

  • Erstellt einen TraceDriver mit einer TraceSink, die Perfetto-Trace-Dateien in Context.noBackupFilesDir/perfetto_traces/ schreibt.

  • Registriert den resultierenden Tracer global.

TraceDriver-Instanz anpassen

Wenn Sie die Konfiguration anpassen müssen, z. B. um den Speicherort für Trace-Dateien zu ändern oder eine benutzerdefinierte TraceSink zu verwenden, können Sie eine eigene AbstractTraceDriver-Instanz bereitstellen.

Um die Konfiguration anzupassen, 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 Treiber-Factory.

JVM

Auf der JVM gibt es keinen automatischen Bootstrapping-Mechanismus. Die Anwendung ist dafür verantwortlich, den TraceDriver zu initialisieren und den Tracer beim Start global zu registrieren, meist in der main-Funktion.

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 einer Senke, die das Perfetto Trace-Paketformat verwendet. Ein TraceDriver stellt ein Handle für den Tracer bereit und kann verwendet werden, um einen Trace abzuschließen.

Sobald der Tracer initialisiert ist, entweder automatisch unter Android oder manuell auf der JVM, verwenden Sie die globale Tracer.global-Instanz, um Trace-Ereignisse auszugeben.

Sie können auch den TraceDriver verwenden, um alle Trace-Punkte in der Anwendung zu deaktivieren, wenn Sie in einigen Anwendungsvarianten kein Tracing durchführen möchten. Optional können Sie Trace-Punkte für eine bestimmte 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 die Ausgabe eines Trace-Ereignisses mit Tracer.global auf 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.

Screenshot eines einfachen Perfetto-Traces

Abbildung 1 : Screenshot eines einfachen Perfetto-Traces.

Sie sehen, dass die richtigen Prozess- und Thread-Tracks ausgefüllt sind und dass sie einen einzelnen Trace-Abschnitt basic erzeugt 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.

Screenshot eines einfachen Perfetto-Traces mit verschachtelten Abschnitten

Abbildung 2 : Screenshot eines einfachen Perfetto-Traces mit verschachtelten Abschnitten.

Sie sehen, dass es im Haupt-Thread-Track überschneidende Ereignisse gibt. Es ist ganz klar, dass processImage loadImage und sharpen im selben Thread aufruft.

Zusätzliche Metadaten in Trace-Abschnitten hinzufügen

Manchmal kann es nützlich sein, einem Trace-Slice zusätzliche Kontextmetadaten anzuhängen, um weitere Details zu erhalten. Beispiele für solche Metadaten sind das nav destination, auf dem sich der Nutzer befindet, oder input arguments, die die Dauer der Ausführung 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)
        }
    }
}

Dadurch wird das folgende Ergebnis erzeugt. Der Abschnitt Arguments enthält Schlüssel/Wert-Paare, die beim Erstellen des slice hinzugefügt wurden.

Screenshot eines einfachen Perfetto-Traces mit zusätzlichen Metadaten

Abbildung 3 : Screenshot eines einfachen Perfetto-Traces mit zusätzlichen Metadaten.

Kontextweitergabe

Bei Verwendung von Kotlin-Coroutinen oder anderen ähnlichen Frameworks, 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")
    }
}

Dadurch wird das folgende Ergebnis erzeugt.

Screenshot eines Perfetto-Traces mit Kontextweitergabe

Abbildung 4 : Screenshot eines einfachen Perfetto-Traces mit Kontextweitergabe.

Die Kontextweitergabe vereinfacht die Visualisierung des Ausführungsablaufs erheblich. Sie können genau sehen, welche Aufgaben miteinander verbunden waren, und genau, wann Threads angehalten und fortgesetzt wurden.

Beispielsweise können Sie sehen, dass der Slice main taskOne und taskTwo erzeugt hat. Danach waren beide Threads inaktiv, da die Coroutinen aufgrund der Verwendung von delay angehalten wurden.

Manuelle Weitergabe

Manchmal, wenn Sie gleichzeitige Arbeitslasten mit Kotlin-Coroutinen mit Instanzen von Java Executor mischen, kann es nützlich sein, den Kontext von einer zur anderen weiterzugeben. 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()
    }
}

Dadurch wird das folgende Ergebnis erzeugt.

Screenshot eines Perfetto-Traces mit manueller Kontextweitergabe

Abbildung 5 : Screenshot eines einfachen Perfetto-Traces mit manueller Kontextweitergabe.

Sie sehen, dass die Ausführung in einem CoroutineContext gestartet und anschließend zu einem Java Executor gewechselt wurde, aber wir konnten trotzdem die Kontextweitergabe verwenden.

Mit System-Traces kombinieren

Die Bibliothek androidx.tracing erfasst keine Informationen wie CPU-Scheduling, Speichernutzung und die Interaktion der Anwendung mit dem Betriebssystem im Allgemeinen. Das liegt daran, dass die Bibliothek eine Möglichkeit bietet, ressourcenschonendes In-Process-Tracing durchzuführen.

Es ist jedoch sehr einfach, System-Traces mit In-Process-Traces zusammenzuführen und sie bei Bedarf als einen einzigen Trace zu visualisieren. Das liegt daran, dass die Perfetto UI die Visualisierung mehrerer Trace-Dateien 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 der Anleitung hier.

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 in Perfetto die Option Open Multiple Trace Files (Mehrere Trace-Dateien öffnen) verwenden.

Mehrere Tracedateien in der Perfetto-Benutzeroberfläche öffnen

Abbildung 6 : Mehrere Trace-Dateien 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.

Slices korrelieren

Manchmal ist es nützlich, Slices in einem Trace einer übergeordneten Nutzeraktion oder einem Systemereignis zuzuordnen. Wenn Sie beispielsweise alle Slices, die einer Hintergrundaufgabe entsprechen, als Teil einer Benachrichtigung 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)
    }
}

Dadurch wird das folgende Ergebnis erzeugt.

Screenshot eines Perfetto-Traces mit korrelierten Slices

Abbildung 7 : Screenshot eines Perfetto-Traces mit korrelierten Slices.

Aufrufstackinformationen hinzufügen

Hostseitige Tools wie Compiler-Plug-ins und Annotationsprozessoren können auch Aufrufstackinformationen in einen Trace einbetten, um die Datei, Klasse oder Methode zu finden, die für die Erstellung eines Trace-Abschnitts in einem Trace verantwortlich ist.

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

Dadurch wird das folgende Ergebnis erzeugt.

Screenshot eines Perfetto-Traces mit Informationen zum Aufrufstack

Abbildung 8 : Screenshot eines Perfetto-Traces mit Aufrufstackinformationen.