Śledzenie w trakcie przetwarzania

Biblioteka androidx.tracing:tracing:2.0.0-beta01 to lekki interfejs Kotlin API, który umożliwia rejestrowanie zdarzeń śledzenia w procesie. Te zdarzenia mogą rejestrować przedziały czasu i ich kontekst. Biblioteka obsługuje też propagację kontekstu w przypadku korutyn Kotlin.

Biblioteka używa tego samego Perfetto formatu pakietów śledzenia, który jest znany deweloperom Androida. Ponadto śledzenie 2.0 (w przeciwieństwie do interfejsów API 1.0.0-*) obsługuje koncepcję wtykowych backendów śledzenia i ujść, dzięki czemu inne biblioteki śledzenia mogą dostosowywać format śledzenia wyjściowego oraz sposób działania propagacji kontekstu w swojej implementacji.

Zależności

Aby rozpocząć śledzenie, musisz zdefiniować zależności w pliku build.gradle.kts.

Projekty Kotlin Multiplatform

Biblioteki, które muszą tylko emitować zdarzenia śledzenia, powinny zależeć od lekkiego interfejsu androidx.tracing:tracing API. Aplikacje, które konfigurują backend śledzenia, powinny też zależeć od 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")
      }
    }
  }
}

Projekty tylko na Androida

Jeśli kierujesz reklamy tylko na Androida, dodaj ten kod do pliku build.gradle.kts aplikacji lub biblioteki:

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

Inicjowanie i wykrywanie

Zanim zaczniesz rejestrować zdarzenia śledzenia, musisz zainicjować infrastrukturę śledzenia. Obejmuje to utworzenie AbstractTraceDriver i zarejestrowanie jego Tracer globalnie.

Android

Jeśli na Androidzie uwzględnisz zależność androidx.tracing:tracing-wire, inicjowanie nastąpi automatycznie podczas uruchamiania aplikacji za pomocą biblioteki androidx.startup.

Domyślnie to automatyczne inicjowanie wykonuje te czynności:

  • Tworzy TraceDriver z TraceSink, który zapisuje pliki śledzenia Perfetto w Context.noBackupFilesDir/perfetto_traces/.

  • Rejestruje wynikowy Tracer globalnie.

Dostosowywanie instancji TraceDriver

Jeśli musisz dostosować konfigurację, np. zmienić miejsce zapisywania plików śledzenia lub użyć niestandardowego TraceSink, możesz podać własną instancję AbstractTraceDriver.

Aby dostosować konfigurację, spraw, aby klasa Application implementowała 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)
    }
}

Automatyczny inicjator wykrywa, że podklasa Application implementuje Factory, i używa Twojej niestandardowej fabryki sterowników.

JVM

W JVM nie ma automatycznego mechanizmu bootstrapowania. Aplikacja jest odpowiedzialna za zainicjowanie TraceDriver i zarejestrowanie Tracer globalnie podczas uruchamiania, najczęściej w funkcji main.

Aby zarejestrować śledzenie, wywołaj 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()
    })
}

Podstawowe użycie

TraceSink określa sposób serializacji pakietów śledzenia. Śledzenie 2.0.0 zawiera implementację ujścia, która używa formatu pakietów śledzenia Perfetto. TraceDriver udostępnia uchwyt do Tracer i może służyć do finalizowania śledzenia.

Gdy Tracer zostanie zainicjowany (automatycznie na Androidzie lub ręcznie w JVM), użyj globalnej instancji Tracer.global, aby emitować zdarzenia śledzenia.

Możesz też użyć TraceDriver, aby wyłączyć wszystkie punkty śledzenia w aplikacji, jeśli w niektórych wariantach aplikacji nie chcesz w ogóle śledzić. Opcjonalnie możesz włączyć punkty śledzenia dla danej category, podając implementację dla isCategoryEnabled podczas tworzenia instancji TraceDriver.

val driver = TraceDriver(
    sink = sink,
    isCategoryEnabled = { category ->
        // Only enable trace points in the "com.example" package
        category.startsWith("com.example")
    }
)

Oto podstawowy przykład emitowania zdarzenia śledzenia za pomocą Tracer.global w JVM, w tym konfiguracja ręczna:

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

Spowoduje to wygenerowanie tego śledzenia.

Zrzut ekranu podstawowego śledzenia Perfetto

Rysunek 1. Zrzut ekranu podstawowego śledzenia Perfetto.

Widać, że wypełnione są prawidłowe ścieżki procesu i wątku oraz że utworzyły one jedną sekcję śledzenia basic, która trwała 100 ms.

Sekcje śledzenia (lub paski) można zagnieżdżać na tej samej ścieżce, aby reprezentować nakładające się zdarzenia. Oto przykład.

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") {
        // ...
    }
}

Spowoduje to wygenerowanie tego śledzenia.

Zrzut ekranu podstawowego śladu Perfetto z zagnieżdżonymi sekcjami

Rysunek 2. Zrzut ekranu podstawowego śledzenia Perfetto z zagnieżdżonymi sekcjami.

Widać, że na ścieżce głównego wątku występują nakładające się zdarzenia. Wyraźnie widać, że processImage wywołuje loadImage i sharpen w tym samym wątku.

Dodawanie dodatkowych metadanych w sekcjach śledzenia

Czasami warto dołączyć do paska śledzenia dodatkowe metadane kontekstowe, aby uzyskać więcej szczegółów. Przykłady takich metadanych to nav destination w którym znajduje się użytkownik, lub input arguments które mogą ostatecznie określić czas trwania funkcji.

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

Spowoduje to wygenerowanie tego wyniku. Zwróć uwagę, że sekcja Arguments zawiera pary klucz wartość dodane podczas tworzenia slice.

Zrzut ekranu podstawowego śladu Perfetto z dodatkowymi metadanymi

Rysunek 3. Zrzut ekranu podstawowego śledzenia Perfetto z dodatkowymi metadanymi.

Propagacja kontekstu

Podczas korzystania z korutyn Kotlin lub innych podobnych frameworków, które ułatwiają obsługę równoczesnych zadań, śledzenie 2.0 obsługuje koncepcję propagacji kontekstu. Najlepiej wyjaśnić to na przykładzie.

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

Spowoduje to wygenerowanie tego wyniku.

Zrzut ekranu śladu Perfetto z propagacją kontekstu

Rysunek 4. Zrzut ekranu podstawowego śledzenia Perfetto z propagacją kontekstu.

Propagacja kontekstu znacznie ułatwia wizualizację przepływu wykonywania. Możesz dokładnie zobaczyć, które zadania były powiązane (połączone z innymi), oraz kiedy dokładnie Threads zostały zawieszone i wznowione.

Możesz na przykład zobaczyć, że pasek main utworzył taskOne i taskTwo. Następnie oba wątki były nieaktywne, ponieważ korutyny zostały zawieszone z powodu użycia delay.

Propagacja ręczna

Czasami, gdy łączysz równoczesne zadania za pomocą korutyn Kotlin z instancjami Java Executor, może się przydać propagacja kontekstu z jednego do drugiego. Oto przykład:

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

Spowoduje to wygenerowanie tego wyniku.

Zrzut ekranu śladu Perfetto z ręcznym propagowaniem kontekstu

Rysunek 5. Zrzut ekranu podstawowego śledzenia Perfetto z ręczną propagacją kontekstu.

Widać, że wykonywanie rozpoczęło się w CoroutineContext, a następnie przełączyło się na Java Executor, ale nadal mogliśmy używać propagacji kontekstu.

Łączenie ze śledzeniem systemu

Biblioteka androidx.tracing nie rejestruje informacji takich jak planowanie procesora, wykorzystanie pamięci i ogólna interakcja aplikacji z systemem operacyjnym. Dzieje się tak, ponieważ biblioteka umożliwia śledzenie w procesie z niskim narzutem.

W razie potrzeby można jednak bardzo łatwo połączyć śledzenie systemu ze śledzeniem w procesie i wizualizować je jako jedno śledzenie. Dzieje się tak, ponieważ Perfetto UI obsługuje wizualizację wielu plików śledzenia z urządzenia na ujednoliconej osi czasu.

Aby to zrobić, możesz rozpocząć sesję śledzenia systemu za pomocą Perfetto UI przez postępowanie zgodnie z instrukcjami tutaj.

Możesz też rejestrować zdarzenia śledzenia w procesie za pomocą interfejsu Tracing 2.0 API, gdy śledzenie systemu jest włączone. Gdy masz oba pliki śledzenia, możesz użyć opcji Open Multiple Trace Files w Perfetto.

Otwieranie wielu plików śledzenia w interfejsie Perfetto

Rysunek 6. Otwieranie wielu plików śledzenia w Perfetto UI.

Zaawansowane przepływy pracy

W tej sekcji opisujemy zaawansowane przepływy pracy, które możesz zaimplementować za pomocą biblioteki śledzenia w procesie.

Korelacja pasków

Czasami warto przypisać paski w śledzeniu do działania użytkownika wyższego poziomu lub zdarzenia systemowego. Aby na przykład przypisać wszystkie paski odpowiadające pracy w tle w ramach powiadomienia, możesz zrobić coś takiego:

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

Spowoduje to wygenerowanie tego wyniku.

Zrzut ekranu zrzutu Perfetto ze skorelowanymi wycinkami

Rysunek 7. Zrzut ekranu śledzenia Perfetto z powiązanymi paskami.

Dodawanie informacji o stosie wywołań

Narzędzia po stronie hosta, takie jak wtyczki kompilatora i procesory adnotacji, mogą też osadzać informacje o stosie wywołań w śledzeniu, aby ułatwić znajdowanie pliku, klasy lub metody odpowiedzialnej za utworzenie sekcji śledzenia.

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

Spowoduje to wygenerowanie tego wyniku.

Zrzut ekranu z zrzutu Perfetto z informacjami o stosie wywołań

Rysunek 8. Zrzut ekranu śledzenia Perfetto z informacjami o stosie wywołań.