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
TraceDrivermit einerTraceSink, die Perfetto-Trace-Dateien inContext.noBackupFilesDir/perfetto_traces/schreibt.Registriert den resultierenden
Tracerglobal.
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.
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.
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.
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.
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.
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.
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.
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.
Abbildung 8 : Screenshot eines Perfetto-Traces mit Aufrufstackinformationen.