Die neue androidx.tracing:tracing:2.0.0-alpha04 Bibliothek ist eine Kotlin API mit geringem Overhead
, mit der Trace-Ereignisse im Prozess erfasst werden können. Mit diesen Ereignissen können Zeitabschnitte und ihr Kontext erfasst werden. Die Bibliothek unterstützt außerdem 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 anpassen können und auch die Funktionsweise der Kontext
weitergabe in ihrer Implementierung.
Abhängigkeiten
Um mit dem Tracing zu beginnen, müssen Sie die folgenden Abhängigkeiten in Ihrer build.gradle.kts definieren.
kotlin {
androidLibrary {
namespace = "com.example.library"
// ...
}
sourceSets {
androidMain {
dependencies {
api("androidx.tracing:tracing-wire:2.0.0-alpha04")
// ...
}
}
jvmMain {
dependencies {
api("androidx.tracing:tracing-wire:2.0.0-alpha04")
// ...
}
}
}
}
Deklarieren Sie eine Abhängigkeit von androidx.tracing:tracing-wire:2.0.0-alpha04, wenn Sie auf eine Android-Bibliothek, eine Android-Anwendung oder die JVM abzielen.
Grundlegende Verwendung
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 bietet ein Handle für den Tracer und kann verwendet werden, um einen Trace abzuschließen.
Sie können den TraceDriver auch verwenden, um alle Trace-Punkte in der Anwendung zu deaktivieren, wenn Sie in einigen Anwendungsvarianten kein Tracing durchführen möchten.
Mit zukünftigen APIs im TraceDriver können Entwickler auch steuern, welche Trace-Kategorien sie erfassen möchten (oder deaktivieren, wenn eine Kategorie zu viele Daten erzeugt).
Erstellen Sie zuerst eine Instanz von TraceSink und TraceDriver.
/**
* A [TraceSink] defines how traces are serialized.
*
* [androidx.tracing.wire.TraceSink] uses the `Perfetto` trace packet format.
*/
fun createSink(): TraceSink {
val outputDirectory = File(/* path = */ "/tmp/perfetto")
if (!outputDirectory.exists()) {
outputDirectory.mkdirs()
}
// We are using the factory function defined in androidx.tracing.wire
return TraceSink(
sequenceId = 1,
directory = outputDirectory
)
}
/**
* Creates a new instance of [androidx.tracing.wire.TraceDriver].
*/
fun createTraceDriver(): TraceDriver {
// We are using a factory function from androidx.tracing.wire here.
// `isEnabled` controls whether tracing is enabled for the application.
val driver = TraceDriver(sink = createSink(), isEnabled = true)
return driver
}
Nachdem Sie eine Instanz von TraceDriver haben, rufen Sie den Tracer ab, der den Einstiegspunkt für alle Tracing-APIs definiert.
// Tracing Categories identify subsystems that are responsible
// in generating trace sections. Future APIs in `TraceDriver` will allow the
// application to specify which categories they are interested in tracing.
// This lets the application disable entire trace categories, without
// needing to disable trace instrumentation at the call sites for those
// categories.
internal const val CATEGORY_MAIN = "main"
fun main() {
val driver = createTraceDriver()
driver.use {
it.tracer.trace(category = CATEGORY_MAIN, name = "basic") {
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 ein einzelner Trace-Abschnitt basic erstellt wurde, der 100ms 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() {
// Initialize the tracing infrastructure to monitor app performance
val driver = createTraceDriver()
val tracer = driver.tracer
driver.use {
it.tracer.trace(
category = CATEGORY_MAIN,
name = "processImage",
) {
// Load the data first, then apply the sharpen filter
sharpen(tracer = tracer, output = loadImage(tracer))
}
}
}
internal fun loadImage(tracer: Tracer): ByteArray {
return tracer.trace(CATEGORY_MAIN, "loadImage") {
// Loads an image
// ...
// A placeholder
ByteArray(0)
}
}
internal fun sharpen(tracer: Tracer, output: ByteArray) {
// ...
tracer.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()
driver.use {
it.tracer.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: Tracer) {
tracer.traceCoroutine(category = CATEGORY_MAIN, "taskOne") {
delay(timeMillis = 100L)
}
}
suspend fun taskTwo(tracer: Tracer) {
tracer.traceCoroutine(category = CATEGORY_MAIN, "taskTwo") {
delay(timeMillis = 50L)
}
}
fun main() = runBlocking(context = Dispatchers.Default) {
val driver = createTraceDriver()
val tracer = driver.tracer
driver.use {
it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
coroutineScope {
launch { taskOne(tracer) }
launch { taskTwo(tracer) }
}
}
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 verknüpft waren und wann Threads angehalten und fortgesetzt wurden.
Beispielsweise sehen Sie, 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
Wenn Sie gleichzeitige Arbeitslasten mit Kotlin-Coroutinen und Instanzen von Java Executor kombinieren, kann es manchmal nützlich sein, den Kontext von einer zur anderen weiterzugeben. Hier ein Beispiel:
fun executorTask(
tracer: Tracer,
token: PropagationToken,
executor: Executor,
callback: () -> Unit
) {
executor.execute {
tracer.trace(
category = CATEGORY_MAIN,
name = "executeTask",
token = token,
) {
// Do something
Thread.sleep(100)
callback()
}
}
}
@OptIn(DelicateTracingApi::class)
fun main() = runBlocking(context = Dispatchers.Default) {
val driver = createTraceDriver()
val executor = Executors.newSingleThreadExecutor()
val tracer = driver.tracer
driver.use {
it.tracer.traceCoroutine(category = CATEGORY_MAIN, name = "main") {
coroutineScope {
val deferred = CompletableDeferred<Unit>()
executorTask(
tracer = tracer,
// Obtain the propagation token from the CoroutineContext
token = tracer.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 neue 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, Tracing im Prozess mit sehr geringem Overhead durchzuführen.
Es ist jedoch sehr einfach, System-Traces mit Traces im Prozess 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 Trace-Ereignisse im Prozess 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 nutzen
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()
onEvent(driver, eventId = EVENT_ID)
}
fun onEvent(driver: TraceDriver, eventId: Long) {
driver.use {
it.tracer.trace(
category = CATEGORY_MAIN,
name = "step-1",
metadataBlock = {
addCorrelationId(eventId)
}
) {
Thread.sleep(100L)
}
Thread.sleep(20)
driver.tracer.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 (Compiler-Plug-ins, Annotation Processors usw.) können zusätzlich 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()
driver.use {
it.tracer.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.